2025-11-17 13:13:32 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2, Download } from "lucide-react";
|
|
|
|
|
import React, { useEffect, useState, useCallback, startTransition, memo } from "react";
|
|
|
|
|
import AnaliticoComponent from "./analitico";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
import * as XLSX from "xlsx";
|
|
|
|
|
import {
|
|
|
|
|
Sheet,
|
|
|
|
|
SheetContent,
|
|
|
|
|
SheetDescription,
|
|
|
|
|
SheetFooter,
|
|
|
|
|
SheetHeader,
|
|
|
|
|
SheetTitle,
|
|
|
|
|
SheetTrigger,
|
|
|
|
|
} from "@/components/ui/sheet";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
|
|
|
|
|
interface DREItem {
|
|
|
|
|
codfilial: string;
|
|
|
|
|
data_competencia: string;
|
|
|
|
|
data_cai: string;
|
|
|
|
|
grupo: string;
|
|
|
|
|
subgrupo: string;
|
|
|
|
|
centro_custo: string;
|
|
|
|
|
codigo_centro_custo: string;
|
|
|
|
|
codigo_conta: number;
|
|
|
|
|
conta: string;
|
|
|
|
|
valor: string;
|
|
|
|
|
codgrupo?: string;
|
|
|
|
|
isCalculado?: boolean;
|
|
|
|
|
entidades?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HierarchicalRow {
|
|
|
|
|
type: "entidade" | "direto_indireto" | "centro_custo" | "conta";
|
|
|
|
|
level: number;
|
|
|
|
|
entidade?: string;
|
|
|
|
|
direto_indireto?: string; // "DIRETO" ou "INDIRETO" (vem do SUBGRUPO)
|
|
|
|
|
centro_custo?: string;
|
|
|
|
|
codigo_centro_custo?: string;
|
|
|
|
|
conta?: string;
|
|
|
|
|
codigo_conta?: number;
|
|
|
|
|
total?: number;
|
|
|
|
|
isExpanded?: boolean;
|
|
|
|
|
valoresPorMes?: Record<string, number>;
|
|
|
|
|
percentuaisPorMes?: Record<string, number>;
|
|
|
|
|
percentualTotal?: number;
|
|
|
|
|
// Campos legados para compatibilidade (não usados na nova hierarquia)
|
|
|
|
|
grupo?: string;
|
|
|
|
|
subgrupo?: string;
|
|
|
|
|
isCalculado?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Componente memoizado para linhas da tabela
|
|
|
|
|
const TableRow = memo(({
|
|
|
|
|
row,
|
|
|
|
|
index,
|
|
|
|
|
handleRowClick,
|
|
|
|
|
getRowStyle,
|
|
|
|
|
getIndentStyle,
|
|
|
|
|
renderCellContent,
|
|
|
|
|
mesesDisponiveis,
|
|
|
|
|
formatCurrency,
|
|
|
|
|
formatCurrencyWithColor,
|
|
|
|
|
getFixedCellBackground
|
|
|
|
|
}: {
|
|
|
|
|
row: HierarchicalRow;
|
|
|
|
|
index: number;
|
|
|
|
|
handleRowClick: (row: HierarchicalRow, mes?: string) => void;
|
|
|
|
|
getRowStyle: (row: HierarchicalRow) => string;
|
|
|
|
|
getIndentStyle: (level: number) => React.CSSProperties;
|
|
|
|
|
renderCellContent: (row: HierarchicalRow) => React.ReactNode;
|
|
|
|
|
mesesDisponiveis: string[];
|
|
|
|
|
formatCurrency: (value: number) => string;
|
|
|
|
|
formatCurrencyWithColor: (value: number) => { formatted: string; isNegative: boolean };
|
|
|
|
|
getFixedCellBackground: (row: HierarchicalRow) => string;
|
|
|
|
|
}) => {
|
|
|
|
|
return (
|
|
|
|
|
<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 [expandedEntidades, setExpandedEntidades] = useState<Set<string>>(new Set());
|
|
|
|
|
const [expandedDiretoIndireto, setExpandedDiretoIndireto] = 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 [entidadesSelecionadas, setEntidadesSelecionadas] = useState<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[]>([]);
|
|
|
|
|
const [opcoesEntidades, setOpcoesEntidades] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
// Estados para filtros de busca nos campos de seleção
|
|
|
|
|
const [filtroCentroCusto, setFiltroCentroCusto] = useState<string>("");
|
|
|
|
|
const [filtroConta, setFiltroConta] = useState<string>("");
|
|
|
|
|
const [filtroEntidade, setFiltroEntidade] = 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-entidade-oracle");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro HTTP: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dadosCompletos = await response.json();
|
|
|
|
|
|
|
|
|
|
// Extrair períodos únicos dos dados
|
|
|
|
|
const periodosUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.data_competencia))].sort() as string[];
|
|
|
|
|
setMesesDisponiveis(periodosUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair grupos únicos
|
|
|
|
|
const gruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.grupo))].sort() as string[];
|
|
|
|
|
setOpcoesGrupos(gruposUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair subgrupos únicos
|
|
|
|
|
const subgruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.subgrupo))].sort() as string[];
|
|
|
|
|
setOpcoesSubgrupos(subgruposUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair centros de custo únicos com nome e código
|
|
|
|
|
const centrosCustoUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.centro_custo))].sort() as string[];
|
|
|
|
|
|
2025-11-19 21:08:03 +00:00
|
|
|
// Filtrar centro de custo 999.998 da lista de opções (mas sempre manter selecionado)
|
|
|
|
|
const centrosCustoParaExibir = centrosCustoUnicos.filter(centro => {
|
|
|
|
|
const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro);
|
|
|
|
|
return item?.codigo_centro_custo !== "999.998";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setOpcoesCentrosCusto(centrosCustoParaExibir);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// 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[];
|
2025-11-19 21:08:03 +00:00
|
|
|
|
|
|
|
|
// Filtrar conta 199 da lista de opções (mas sempre manter selecionada)
|
|
|
|
|
const contasParaExibir = contasUnicas.filter(conta => {
|
|
|
|
|
const item = dadosCompletos.find((d: DREItem) => d.conta === conta);
|
|
|
|
|
return item?.codigo_conta?.toString() !== "199";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setOpcoesContas(contasParaExibir);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
// Extrair entidades únicas
|
|
|
|
|
const entidadesUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.entidades).filter(Boolean))].sort() as string[];
|
|
|
|
|
console.log('🏢 Entidades únicas encontradas:', entidadesUnicas);
|
|
|
|
|
console.log('📊 Total de entidades:', entidadesUnicas.length);
|
2025-11-19 21:08:03 +00:00
|
|
|
|
|
|
|
|
// Filtrar "Faturamento líquido" da lista de opções (mas sempre manter selecionado)
|
|
|
|
|
const entidadeFaturamentoLiquido = entidadesUnicas.find(ent =>
|
|
|
|
|
ent.toLowerCase().includes('faturamento') && ent.toLowerCase().includes('líquido')
|
|
|
|
|
) || entidadesUnicas.find(ent =>
|
|
|
|
|
ent.toLowerCase().includes('faturamento liquido')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const entidadesParaExibir = entidadesUnicas.filter(ent => ent !== entidadeFaturamentoLiquido);
|
|
|
|
|
setOpcoesEntidades(entidadesParaExibir);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// Inicializar com todos os itens selecionados, exceto o centro de custo 002.003.017 e conta 100050
|
2025-11-19 21:08:03 +00:00
|
|
|
// Mas sempre incluir centro de custo 999.998 e conta 199
|
2025-11-17 13:13:32 +00:00
|
|
|
const centrosCustoIniciaisSelecionados = centrosCustoUnicos.filter(centro => {
|
|
|
|
|
const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro);
|
2025-11-19 21:08:03 +00:00
|
|
|
// Sempre incluir centro de custo com código 999.998
|
|
|
|
|
if (item?.codigo_centro_custo === "999.998") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Excluir centro de custo 002.003.017
|
2025-11-17 13:13:32 +00:00
|
|
|
return item?.codigo_centro_custo !== "002.003.017";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const contasIniciaisSelecionadas = contasUnicas.filter(conta => {
|
|
|
|
|
const item = dadosCompletos.find((d: DREItem) => d.conta === conta);
|
2025-11-19 21:08:03 +00:00
|
|
|
// Sempre incluir conta com código 199
|
|
|
|
|
if (item?.codigo_conta?.toString() === "199") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Excluir conta 100050
|
2025-11-17 13:13:32 +00:00
|
|
|
return item?.codigo_conta?.toString() !== "100050";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setCentrosCustoSelecionados(centrosCustoIniciaisSelecionados);
|
|
|
|
|
setContasSelecionadas(contasIniciaisSelecionadas);
|
|
|
|
|
|
2025-11-19 21:08:03 +00:00
|
|
|
// Inicializar com todas as entidades selecionadas (incluindo Faturamento líquido)
|
2025-11-17 13:13:32 +00:00
|
|
|
setEntidadesSelecionadas(entidadesUnicas);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Erro ao carregar períodos:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
const response = await fetch("/api/dre-entidade-oracle");
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro ao carregar dados: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
setData(result);
|
|
|
|
|
|
|
|
|
|
// Extrair meses únicos dos dados
|
|
|
|
|
const meses = [
|
|
|
|
|
...new Set(
|
|
|
|
|
result.map((item: DREItem) => {
|
|
|
|
|
// Usar diretamente o valor de data_competencia que já vem no formato YYYY-MM
|
|
|
|
|
return item.data_competencia;
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
].sort() as string[];
|
|
|
|
|
|
|
|
|
|
setMesesDisponiveis(meses);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "Erro desconhecido");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (value: string | number) => {
|
|
|
|
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
|
|
|
|
return numValue.toLocaleString("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatCurrencyWithColor = (value: string | number) => {
|
|
|
|
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
|
|
|
|
const formatted = formatCurrency(value);
|
|
|
|
|
const isNegative = numValue < 0;
|
|
|
|
|
return { formatted, isNegative };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função para lidar com clique nas linhas
|
|
|
|
|
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
|
|
|
|
|
console.log('🖱️ Clique na linha:', row);
|
|
|
|
|
console.log('📅 Mês selecionado:', mesSelecionado);
|
|
|
|
|
|
|
|
|
|
if (!data.length) {
|
|
|
|
|
console.log('⚠️ Sem dados disponíveis');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pegar todas as datas disponíveis para definir o período
|
|
|
|
|
const datas = data.map((item) => item.data_competencia);
|
|
|
|
|
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
|
|
|
|
|
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
|
|
|
|
|
|
|
|
|
|
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7); // YYYY-MM
|
|
|
|
|
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7); // YYYY-MM
|
|
|
|
|
|
|
|
|
|
console.log('📅 Datas calculadas:', { dataInicioStr, dataFimStr });
|
|
|
|
|
|
|
|
|
|
// Criar um identificador único para a linha
|
|
|
|
|
const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${
|
|
|
|
|
row.centro_custo || ""
|
|
|
|
|
}-${row.codigo_conta || ""}`;
|
|
|
|
|
setLinhaSelecionada(linhaId);
|
|
|
|
|
|
|
|
|
|
// Se um mês específico foi selecionado, usar apenas esse mês
|
|
|
|
|
const dataInicioFiltro = mesSelecionado || dataInicioStr;
|
|
|
|
|
const dataFimFiltro = mesSelecionado || dataFimStr;
|
|
|
|
|
|
|
|
|
|
// Determinar filtros baseado na nova hierarquia [entidade, direto/indireto, cc, conta]
|
|
|
|
|
let centroCustoFiltro = "";
|
|
|
|
|
let codigoContaFiltro = "";
|
2025-11-21 02:29:07 +00:00
|
|
|
let codigoGrupoFiltro = "";
|
2025-11-17 13:13:32 +00:00
|
|
|
let entidadeFiltro = "";
|
|
|
|
|
let diretoIndiretoFiltro = "";
|
|
|
|
|
|
|
|
|
|
// Sempre filtrar por entidade se disponível
|
|
|
|
|
entidadeFiltro = row.entidade || "";
|
|
|
|
|
|
|
|
|
|
// Filtrar por direto/indireto se disponível
|
2025-11-21 02:35:29 +00:00
|
|
|
// Quando clicar em uma célula de valor de uma linha "direto_indireto" (DIRETO, INDIRETO ou SEM CC),
|
|
|
|
|
// adicionar o SUBGRUPO como filtro adicional
|
2025-11-17 13:13:32 +00:00
|
|
|
diretoIndiretoFiltro = row.direto_indireto || "";
|
|
|
|
|
|
2025-11-21 02:35:29 +00:00
|
|
|
// Verificar se a linha é do tipo "direto_indireto" e se a entidade pai está expandida (drill aberto)
|
|
|
|
|
// Se sim, usar o direto_indireto como codigoSubgrupo no filtro
|
|
|
|
|
let codigoSubgrupoFiltro = "";
|
|
|
|
|
if (row.type === "direto_indireto" && row.direto_indireto) {
|
|
|
|
|
// Verificar se a entidade pai está expandida
|
|
|
|
|
const entidadePaiExpandida = expandedEntidades.has(row.entidade || "");
|
|
|
|
|
if (entidadePaiExpandida) {
|
|
|
|
|
// Usar o direto_indireto (DIRETO, INDIRETO ou SEM CC) como filtro de subgrupo
|
|
|
|
|
codigoSubgrupoFiltro = row.direto_indireto;
|
|
|
|
|
console.log('📊 Drill aberto - Adicionando filtro de subgrupo:', codigoSubgrupoFiltro);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 02:29:07 +00:00
|
|
|
// Buscar CODGRUPO dos dados originais baseado na hierarquia selecionada
|
|
|
|
|
// Quando clicar em qualquer nível (entidade, direto/indireto, centro de custo ou conta), deve filtrar por CODGRUPO
|
|
|
|
|
if (row.type === "entidade" || row.type === "direto_indireto" || row.type === "centro_custo" || row.type === "conta") {
|
|
|
|
|
// Buscar o CODGRUPO dos dados originais que correspondem a esta linha
|
|
|
|
|
// Filtrar também pelo período selecionado se houver
|
|
|
|
|
const itemsCorrespondentes = data.filter((item: DREItem) => {
|
|
|
|
|
// Filtrar por período se um mês específico foi selecionado
|
|
|
|
|
if (mesSelecionado && item.data_competencia !== mesSelecionado) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (row.type === "entidade") {
|
|
|
|
|
return item.entidades === row.entidade;
|
|
|
|
|
} else if (row.type === "direto_indireto") {
|
|
|
|
|
return item.entidades === row.entidade && item.subgrupo === row.direto_indireto;
|
|
|
|
|
} else if (row.type === "centro_custo") {
|
|
|
|
|
return item.entidades === row.entidade &&
|
|
|
|
|
item.subgrupo === row.direto_indireto &&
|
|
|
|
|
item.codigo_centro_custo === row.codigo_centro_custo;
|
|
|
|
|
} else if (row.type === "conta") {
|
|
|
|
|
return item.entidades === row.entidade &&
|
|
|
|
|
item.subgrupo === row.direto_indireto &&
|
|
|
|
|
item.codigo_centro_custo === row.codigo_centro_custo &&
|
|
|
|
|
item.codigo_conta === row.codigo_conta;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Pegar o CODGRUPO do primeiro item encontrado (todos devem ter o mesmo CODGRUPO)
|
|
|
|
|
if (itemsCorrespondentes.length > 0) {
|
|
|
|
|
const primeiroItem = itemsCorrespondentes[0];
|
|
|
|
|
// Priorizar codgrupo direto, depois tentar extrair do campo grupo
|
|
|
|
|
codigoGrupoFiltro = primeiroItem.codgrupo ||
|
|
|
|
|
(primeiroItem.grupo ? primeiroItem.grupo.split(' ')[0]?.trim() : "") ||
|
|
|
|
|
"";
|
|
|
|
|
|
|
|
|
|
console.log('📊 CODGRUPO encontrado para a linha:', {
|
|
|
|
|
codigoGrupoFiltro,
|
|
|
|
|
codgrupo: primeiroItem.codgrupo,
|
|
|
|
|
grupo: primeiroItem.grupo,
|
|
|
|
|
itemsEncontrados: itemsCorrespondentes.length,
|
|
|
|
|
tipoLinha: row.type,
|
|
|
|
|
mesSelecionado: mesSelecionado
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('⚠️ Nenhum item correspondente encontrado para extrair CODGRUPO:', {
|
|
|
|
|
tipoLinha: row.type,
|
|
|
|
|
entidade: row.entidade,
|
|
|
|
|
direto_indireto: row.direto_indireto,
|
|
|
|
|
centro_custo: row.centro_custo,
|
|
|
|
|
codigo_centro_custo: row.codigo_centro_custo,
|
|
|
|
|
codigo_conta: row.codigo_conta,
|
|
|
|
|
mesSelecionado: mesSelecionado
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Filtrar por centro de custo se for nível centro_custo ou conta
|
|
|
|
|
if (row.type === "centro_custo" || row.type === "conta") {
|
|
|
|
|
centroCustoFiltro = row.codigo_centro_custo || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtrar por conta se for nível conta
|
|
|
|
|
if (row.type === "conta") {
|
|
|
|
|
codigoContaFiltro = row.codigo_conta?.toString() || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('🎯 Filtros determinados (nova hierarquia):', {
|
|
|
|
|
entidadeFiltro,
|
|
|
|
|
diretoIndiretoFiltro,
|
2025-11-21 02:29:07 +00:00
|
|
|
codigoGrupoFiltro,
|
2025-11-17 13:13:32 +00:00
|
|
|
centroCustoFiltro,
|
2025-11-19 20:31:54 +00:00
|
|
|
codigoContaFiltro,
|
2025-11-17 13:13:32 +00:00
|
|
|
tipoLinha: row.type,
|
|
|
|
|
rowData: {
|
|
|
|
|
entidade: row.entidade,
|
|
|
|
|
direto_indireto: row.direto_indireto,
|
|
|
|
|
codigo_conta: row.codigo_conta,
|
|
|
|
|
codigo_centro_custo: row.codigo_centro_custo,
|
|
|
|
|
centro_custo: row.centro_custo,
|
|
|
|
|
conta: row.conta
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Determinar exclusões baseado nos filtros aplicados
|
|
|
|
|
let excluirCentroCusto = "";
|
|
|
|
|
let excluirCodigoConta = "";
|
|
|
|
|
|
|
|
|
|
// Se o centro de custo "002.003.017" não está selecionado, excluir da consulta
|
|
|
|
|
const centroCusto002003017Selecionado = centrosCustoSelecionados.some(centro => {
|
|
|
|
|
const codigoCentro = codigosCentrosCusto[centro];
|
|
|
|
|
if (codigoCentro === "002.003.017") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const item = data.find((d: DREItem) => d.centro_custo === centro);
|
|
|
|
|
return item?.codigo_centro_custo === "002.003.017";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!centroCusto002003017Selecionado) {
|
|
|
|
|
excluirCentroCusto = "002.003.017";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Se a conta "100050" não está selecionada, excluir da consulta
|
|
|
|
|
const conta100050Selecionada = contasSelecionadas.some(conta => {
|
|
|
|
|
const codigoConta = codigosContas[conta];
|
|
|
|
|
if (codigoConta === "100050") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const item = data.find((d: DREItem) => d.conta === conta);
|
|
|
|
|
return item?.codigo_conta?.toString() === "100050";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!conta100050Selecionada) {
|
|
|
|
|
excluirCodigoConta = "100050";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Obter códigos dos centros de custo selecionados no filtro
|
|
|
|
|
const codigosCentrosCustoSelecionados = centrosCustoSelecionados
|
|
|
|
|
.map(centro => {
|
|
|
|
|
const codigoDoMapeamento = codigosCentrosCusto[centro];
|
|
|
|
|
if (codigoDoMapeamento) {
|
|
|
|
|
return codigoDoMapeamento;
|
|
|
|
|
}
|
|
|
|
|
const item = data.find((d: DREItem) => d.centro_custo === centro);
|
|
|
|
|
return item?.codigo_centro_custo;
|
|
|
|
|
})
|
|
|
|
|
.filter(codigo => codigo && codigo.trim() !== '')
|
|
|
|
|
.join(',');
|
|
|
|
|
|
|
|
|
|
// Obter códigos das contas selecionadas no filtro
|
|
|
|
|
const codigosContasSelecionadas = contasSelecionadas
|
|
|
|
|
.map(conta => {
|
|
|
|
|
const item = data.find((d: DREItem) => d.conta === conta);
|
|
|
|
|
return item?.codigo_conta?.toString();
|
|
|
|
|
})
|
|
|
|
|
.filter(codigo => codigo)
|
|
|
|
|
.join(',');
|
|
|
|
|
|
|
|
|
|
const novosFiltros = {
|
|
|
|
|
dataInicio: dataInicioFiltro,
|
|
|
|
|
dataFim: dataFimFiltro,
|
|
|
|
|
centroCusto: centroCustoFiltro,
|
2025-11-21 02:29:07 +00:00
|
|
|
codigoGrupo: codigoGrupoFiltro, // CODGRUPO da entidade/linha selecionada
|
2025-11-21 02:35:29 +00:00
|
|
|
codigoSubgrupo: codigoSubgrupoFiltro || diretoIndiretoFiltro, // SUBGRUPO quando drill aberto (DIRETO, INDIRETO ou SEM CC)
|
2025-11-17 13:13:32 +00:00
|
|
|
codigoConta: codigoContaFiltro,
|
|
|
|
|
linhaSelecionada: row.entidade || row.direto_indireto || row.centro_custo || row.conta || "",
|
|
|
|
|
excluirCentroCusto,
|
|
|
|
|
excluirCodigoConta,
|
|
|
|
|
codigosCentrosCustoSelecionados,
|
|
|
|
|
codigosContasSelecionadas,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('🎯 Novos filtros para analítico:', novosFiltros);
|
|
|
|
|
setAnaliticoFiltros(novosFiltros);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleEntidade = useCallback((entidade: string) => {
|
|
|
|
|
setExpandedEntidades(prev => {
|
|
|
|
|
const newExpanded = new Set(prev);
|
|
|
|
|
if (newExpanded.has(entidade)) {
|
|
|
|
|
newExpanded.delete(entidade);
|
2025-11-19 20:31:54 +00:00
|
|
|
} else {
|
2025-11-17 13:13:32 +00:00
|
|
|
newExpanded.add(entidade);
|
2025-11-19 20:31:54 +00:00
|
|
|
}
|
2025-11-17 13:13:32 +00:00
|
|
|
return newExpanded;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const toggleDiretoIndireto = useCallback((chave: string) => {
|
|
|
|
|
setExpandedDiretoIndireto(prev => {
|
|
|
|
|
const newExpanded = new Set(prev);
|
|
|
|
|
if (newExpanded.has(chave)) {
|
|
|
|
|
newExpanded.delete(chave);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(chave);
|
|
|
|
|
}
|
|
|
|
|
return newExpanded;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const toggleCentro = useCallback((chave: string) => {
|
|
|
|
|
setExpandedCentros(prev => {
|
|
|
|
|
const newExpanded = new Set(prev);
|
|
|
|
|
if (newExpanded.has(chave)) {
|
|
|
|
|
newExpanded.delete(chave);
|
2025-11-19 20:31:54 +00:00
|
|
|
} else {
|
2025-11-17 13:13:32 +00:00
|
|
|
newExpanded.add(chave);
|
2025-11-19 20:31:54 +00:00
|
|
|
}
|
2025-11-17 13:13:32 +00:00
|
|
|
return newExpanded;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleFiltroChange = (campo: string, valor: string) => {
|
|
|
|
|
setFiltros(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[campo]: valor
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Funções para multi-seleção
|
|
|
|
|
const toggleCentroCusto = (centro: string) => {
|
2025-11-19 21:08:03 +00:00
|
|
|
// Verificar se é centro de custo 999.998 (não pode ser desmarcado)
|
|
|
|
|
const item = data.find((d: DREItem) => d.centro_custo === centro);
|
|
|
|
|
if (item?.codigo_centro_custo === "999.998") {
|
|
|
|
|
// Não permitir desmarcar centro de custo 999.998
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
setCentrosCustoSelecionados(prev => {
|
|
|
|
|
if (prev.includes(centro)) {
|
|
|
|
|
return prev.filter(c => c !== centro);
|
|
|
|
|
} else {
|
|
|
|
|
return [...prev, centro];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleConta = (conta: string) => {
|
2025-11-19 21:08:03 +00:00
|
|
|
// Verificar se é conta 199 (não pode ser desmarcada)
|
|
|
|
|
const item = data.find((d: DREItem) => d.conta === conta);
|
|
|
|
|
if (item?.codigo_conta?.toString() === "199") {
|
|
|
|
|
// Não permitir desmarcar conta 199
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
setContasSelecionadas(prev => {
|
|
|
|
|
if (prev.includes(conta)) {
|
|
|
|
|
return prev.filter(c => c !== conta);
|
|
|
|
|
} else {
|
|
|
|
|
return [...prev, conta];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selecionarTodosCentros = () => {
|
2025-11-19 21:08:03 +00:00
|
|
|
// Sempre incluir centro de custo 999.998 mesmo que não esteja nas opções
|
|
|
|
|
const centroCusto999998 = data.find((d: DREItem) => d.codigo_centro_custo === "999.998")?.centro_custo;
|
|
|
|
|
const todosCentros = centroCusto999998 && !opcoesCentrosCusto.includes(centroCusto999998)
|
|
|
|
|
? [...opcoesCentrosCusto, centroCusto999998]
|
|
|
|
|
: opcoesCentrosCusto;
|
|
|
|
|
|
|
|
|
|
setCentrosCustoSelecionados(todosCentros);
|
2025-11-17 13:13:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limparCentros = () => {
|
|
|
|
|
setCentrosCustoSelecionados([]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selecionarTodasContas = () => {
|
2025-11-19 21:08:03 +00:00
|
|
|
// Sempre incluir conta 199 mesmo que não esteja nas opções
|
|
|
|
|
const conta199 = data.find((d: DREItem) => d.codigo_conta?.toString() === "199")?.conta;
|
|
|
|
|
const todasContas = conta199 && !opcoesContas.includes(conta199)
|
|
|
|
|
? [...opcoesContas, conta199]
|
|
|
|
|
: opcoesContas;
|
|
|
|
|
|
|
|
|
|
setContasSelecionadas(todasContas);
|
2025-11-17 13:13:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limparContas = () => {
|
|
|
|
|
setContasSelecionadas([]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleEntidadeFiltro = (entidade: string) => {
|
2025-11-19 21:08:03 +00:00
|
|
|
// Verificar se é "Faturamento líquido" (não pode ser desmarcado)
|
|
|
|
|
const isFaturamentoLiquido = entidade.toLowerCase().includes('faturamento') &&
|
|
|
|
|
(entidade.toLowerCase().includes('líquido') || entidade.toLowerCase().includes('liquido'));
|
|
|
|
|
|
|
|
|
|
if (isFaturamentoLiquido) {
|
|
|
|
|
// Não permitir desmarcar Faturamento líquido
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
setEntidadesSelecionadas(prev => {
|
|
|
|
|
if (prev.includes(entidade)) {
|
|
|
|
|
return prev.filter(e => e !== entidade);
|
|
|
|
|
} else {
|
|
|
|
|
return [...prev, entidade];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selecionarTodasEntidades = () => {
|
2025-11-19 21:08:03 +00:00
|
|
|
// Sempre incluir "Faturamento líquido" mesmo que não esteja nas opções
|
|
|
|
|
const entidadeFaturamentoLiquido = data.find((d: DREItem) =>
|
|
|
|
|
d.entidades && (
|
|
|
|
|
d.entidades.toLowerCase().includes('faturamento') &&
|
|
|
|
|
(d.entidades.toLowerCase().includes('líquido') || d.entidades.toLowerCase().includes('liquido'))
|
|
|
|
|
)
|
|
|
|
|
)?.entidades;
|
|
|
|
|
|
|
|
|
|
const todasEntidades = entidadeFaturamentoLiquido && !opcoesEntidades.includes(entidadeFaturamentoLiquido)
|
|
|
|
|
? [...opcoesEntidades, entidadeFaturamentoLiquido]
|
|
|
|
|
: opcoesEntidades;
|
|
|
|
|
|
|
|
|
|
setEntidadesSelecionadas(todasEntidades);
|
2025-11-17 13:13:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limparEntidades = () => {
|
|
|
|
|
setEntidadesSelecionadas([]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função auxiliar para obter o código do centro de custo
|
|
|
|
|
const obterCodigoCentroCusto = React.useCallback((nomeCentro: string): string => {
|
|
|
|
|
if (!data || data.length === 0) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Buscar o primeiro item que corresponde ao nome do centro de custo
|
|
|
|
|
const item = data.find(item => item.centro_custo === nomeCentro);
|
|
|
|
|
|
|
|
|
|
if (item && item.codigo_centro_custo) {
|
|
|
|
|
return item.codigo_centro_custo;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '';
|
|
|
|
|
}, [data]);
|
|
|
|
|
|
|
|
|
|
const exportarXLSX = () => {
|
|
|
|
|
if (!data.length) {
|
|
|
|
|
console.log('⚠️ Nenhum dado para exportar');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('📊 Exportando TODOS os dados expandidos para XLSX...');
|
|
|
|
|
|
|
|
|
|
// Criar uma versão completamente expandida dos dados hierárquicos
|
|
|
|
|
const dadosCompletosExpandidos = buildHierarchicalDataCompleta();
|
|
|
|
|
|
|
|
|
|
// Preparar dados para exportação
|
|
|
|
|
const dadosExportacao = dadosCompletosExpandidos.map((row, index) => {
|
|
|
|
|
const linha: any = {
|
|
|
|
|
'Linha': index + 1,
|
|
|
|
|
'Tipo': row.type,
|
|
|
|
|
'Nível': row.level,
|
|
|
|
|
'Entidade': row.entidade || '',
|
|
|
|
|
'Direto/Indireto': row.direto_indireto || '',
|
|
|
|
|
'Centro de Custo': row.centro_custo || '',
|
|
|
|
|
'Código Centro': row.codigo_centro_custo || '',
|
|
|
|
|
'Conta': row.conta || '',
|
|
|
|
|
'Código Conta': row.codigo_conta || '',
|
|
|
|
|
'Total': row.total || 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Adicionar colunas dos meses
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
const valor = row.valoresPorMes?.[mes] || 0;
|
|
|
|
|
const percentual = row.percentuaisPorMes?.[mes] || 0;
|
|
|
|
|
linha[`Valor ${mes}`] = valor;
|
|
|
|
|
linha[`% ${mes}`] = percentual;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return linha;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Criar workbook
|
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
|
|
|
|
|
|
|
|
// Criar worksheet principal
|
|
|
|
|
const ws = XLSX.utils.json_to_sheet(dadosExportacao);
|
|
|
|
|
|
|
|
|
|
// Ajustar largura das colunas
|
|
|
|
|
const colWidths = [
|
|
|
|
|
{ wch: 8 }, // Linha
|
|
|
|
|
{ wch: 15 }, // Tipo
|
|
|
|
|
{ wch: 8 }, // Nível
|
|
|
|
|
{ wch: 25 }, // Entidade
|
|
|
|
|
{ wch: 20 }, // Direto/Indireto
|
|
|
|
|
{ wch: 25 }, // Centro de Custo
|
|
|
|
|
{ wch: 15 }, // Código Centro
|
|
|
|
|
{ wch: 35 }, // Conta
|
|
|
|
|
{ wch: 12 }, // Código Conta
|
|
|
|
|
{ wch: 15 }, // Total
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Adicionar larguras para colunas dos meses
|
|
|
|
|
mesesDisponiveis.forEach(() => {
|
|
|
|
|
colWidths.push({ wch: 15 }); // Valor
|
|
|
|
|
colWidths.push({ wch: 10 }); // %
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ws['!cols'] = colWidths;
|
|
|
|
|
|
|
|
|
|
// Adicionar worksheet ao workbook
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, 'DRE Gerencial Completo');
|
|
|
|
|
|
|
|
|
|
// Criar worksheet de resumo
|
|
|
|
|
const resumoData = [
|
|
|
|
|
{ 'Informação': 'Período', 'Valor': `${filtros.periodoDe} a ${filtros.periodoAte}` },
|
|
|
|
|
{ 'Informação': 'Grupo', 'Valor': filtros.grupo },
|
|
|
|
|
{ 'Informação': 'Subgrupo', 'Valor': filtros.subgrupo },
|
|
|
|
|
{ 'Informação': 'Centro de Custo', 'Valor': filtros.centroCusto },
|
|
|
|
|
{ 'Informação': 'Conta', 'Valor': filtros.conta },
|
|
|
|
|
{ 'Informação': 'Valor Mínimo', 'Valor': filtros.valorMin || 'N/A' },
|
|
|
|
|
{ 'Informação': 'Valor Máximo', 'Valor': filtros.valorMax || 'N/A' },
|
|
|
|
|
{ 'Informação': 'Busca Textual', 'Valor': filtros.buscaTextual || 'N/A' },
|
|
|
|
|
{ 'Informação': 'Ordem Hierárquica', 'Valor': ordemHierarquiaContasPrimeiro ? 'Contas → Centros' : 'Centros → Contas' },
|
|
|
|
|
{ 'Informação': 'Total de Registros', 'Valor': dadosCompletosExpandidos.length },
|
|
|
|
|
{ 'Informação': 'Data de Exportação', 'Valor': new Date().toLocaleString('pt-BR') },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
|
|
|
|
|
wsResumo['!cols'] = [{ wch: 20 }, { wch: 30 }];
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
|
|
|
|
|
|
|
|
|
|
// Gerar nome do arquivo
|
|
|
|
|
const dataAtual = new Date().toISOString().split('T')[0];
|
|
|
|
|
const nomeArquivo = `DRE_Gerencial_Completo_${dataAtual}.xlsx`;
|
|
|
|
|
|
|
|
|
|
// Exportar arquivo
|
|
|
|
|
XLSX.writeFile(wb, nomeArquivo);
|
|
|
|
|
|
|
|
|
|
console.log('✅ Arquivo XLSX completo exportado:', nomeArquivo);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função para construir dados hierárquicos completamente expandidos (para exportação XLSX)
|
|
|
|
|
// Usa a mesma lógica de buildHierarchicalData mas sempre expandido
|
|
|
|
|
const buildHierarchicalDataCompleta = (): HierarchicalRow[] => {
|
|
|
|
|
const rows: HierarchicalRow[] = [];
|
|
|
|
|
|
|
|
|
|
// Nova hierarquia: [entidade, direto/indireto, cc, conta]
|
2025-11-19 20:31:54 +00:00
|
|
|
// Agrupar primeiro por CODGRUPO, depois por entidade
|
|
|
|
|
const gruposPorCodigo = data.reduce((acc, item) => {
|
|
|
|
|
const codgrupo = item.codgrupo || "";
|
|
|
|
|
if (!codgrupo) return acc;
|
|
|
|
|
if (!acc[codgrupo]) {
|
|
|
|
|
acc[codgrupo] = {};
|
|
|
|
|
}
|
2025-11-17 13:13:32 +00:00
|
|
|
const entidade = item.entidades || "";
|
|
|
|
|
if (!entidade) return acc;
|
2025-11-19 20:31:54 +00:00
|
|
|
if (!acc[codgrupo][entidade]) {
|
|
|
|
|
acc[codgrupo][entidade] = [];
|
2025-11-17 13:13:32 +00:00
|
|
|
}
|
2025-11-19 20:31:54 +00:00
|
|
|
acc[codgrupo][entidade].push(item);
|
2025-11-17 13:13:32 +00:00
|
|
|
return acc;
|
2025-11-19 20:31:54 +00:00
|
|
|
}, {} as Record<string, Record<string, DREItem[]>>);
|
|
|
|
|
|
|
|
|
|
// Ordenar por CODGRUPO (numericamente), depois por entidade (alfabeticamente)
|
|
|
|
|
const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => {
|
|
|
|
|
// Ordenar numericamente por CODGRUPO
|
|
|
|
|
const numA = parseInt(codA) || 0;
|
|
|
|
|
const numB = parseInt(codB) || 0;
|
|
|
|
|
if (numA !== numB) {
|
|
|
|
|
return numA - numB;
|
|
|
|
|
}
|
|
|
|
|
// Se não for numérico, ordenar alfabeticamente
|
|
|
|
|
return codA.localeCompare(codB);
|
|
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
sortedGrupos.forEach(([codgrupo, entidades]) => {
|
|
|
|
|
// Ordenar entidades dentro do grupo alfabeticamente
|
|
|
|
|
const sortedEntidades = Object.entries(entidades).sort(([entA], [entB]) =>
|
|
|
|
|
entA.localeCompare(entB)
|
|
|
|
|
);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
sortedEntidades.forEach(([entidade, items]) => {
|
2025-11-17 13:13:32 +00:00
|
|
|
const totalEntidade = items.reduce((sum, item) => sum + parseFloat(item.valor), 0);
|
|
|
|
|
const valoresEntidadePorMes = calcularValoresPorMes(items);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se a entidade contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquido = items.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculo = temFaturamentoLiquido ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha da entidade (Level 0) - sempre expandida
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "entidade",
|
|
|
|
|
level: 0,
|
|
|
|
|
entidade,
|
|
|
|
|
total: totalEntidade,
|
|
|
|
|
isExpanded: true,
|
|
|
|
|
valoresPorMes: valoresEntidadePorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresEntidadePorMes, codgrupoParaCalculo),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalEntidade, codgrupoParaCalculo),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Agrupar por direto/indireto (SUBGRUPO) dentro da entidade
|
|
|
|
|
const diretoIndireto = items.reduce((acc, item) => {
|
|
|
|
|
const subgrupo = item.subgrupo || "";
|
|
|
|
|
if (!subgrupo) return acc;
|
|
|
|
|
if (!acc[subgrupo]) {
|
|
|
|
|
acc[subgrupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[subgrupo].push(item);
|
2025-11-19 20:31:54 +00:00
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// Ordenar: DIRETO primeiro, depois INDIRETO
|
|
|
|
|
const sortedDiretoIndireto = Object.entries(diretoIndireto).sort(([a], [b]) => {
|
|
|
|
|
if (a.toUpperCase() === "DIRETO" && b.toUpperCase() !== "DIRETO") return -1;
|
|
|
|
|
if (a.toUpperCase() !== "DIRETO" && b.toUpperCase() === "DIRETO") return 1;
|
|
|
|
|
if (a.toUpperCase() === "INDIRETO" && b.toUpperCase() !== "INDIRETO") return -1;
|
|
|
|
|
if (a.toUpperCase() !== "INDIRETO" && b.toUpperCase() === "INDIRETO") return 1;
|
|
|
|
|
return a.localeCompare(b);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedDiretoIndireto.forEach(([diretoIndireto, diretoIndiretoItems]) => {
|
|
|
|
|
const totalDiretoIndireto = diretoIndiretoItems.reduce(
|
2025-11-19 20:31:54 +00:00
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
2025-11-17 13:13:32 +00:00
|
|
|
const valoresDiretoIndiretoPorMes = calcularValoresPorMes(diretoIndiretoItems);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se direto/indireto contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquidoDI = diretoIndiretoItems.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculoDI = temFaturamentoLiquidoDI ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha direto/indireto (Level 1) - sempre expandida
|
2025-11-19 20:31:54 +00:00
|
|
|
rows.push({
|
2025-11-17 13:13:32 +00:00
|
|
|
type: "direto_indireto",
|
2025-11-19 20:31:54 +00:00
|
|
|
level: 1,
|
2025-11-17 13:13:32 +00:00
|
|
|
entidade,
|
|
|
|
|
direto_indireto: diretoIndireto,
|
|
|
|
|
total: totalDiretoIndireto,
|
|
|
|
|
isExpanded: true,
|
|
|
|
|
valoresPorMes: valoresDiretoIndiretoPorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresDiretoIndiretoPorMes, codgrupoParaCalculoDI),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalDiretoIndireto, codgrupoParaCalculoDI),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Agrupar por centro de custo dentro de direto/indireto
|
|
|
|
|
const centros = diretoIndiretoItems.reduce((acc, item) => {
|
|
|
|
|
const centro = item.centro_custo || "";
|
|
|
|
|
if (!centro) return acc;
|
|
|
|
|
if (!acc[centro]) {
|
|
|
|
|
acc[centro] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[centro].push(item);
|
2025-11-19 20:31:54 +00:00
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
// 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 || "";
|
2025-11-17 13:13:32 +00:00
|
|
|
if (codigoA && codigoB) {
|
|
|
|
|
return codigoA.localeCompare(codigoB);
|
|
|
|
|
}
|
|
|
|
|
if (codigoA && !codigoB) return -1;
|
|
|
|
|
if (!codigoA && codigoB) return 1;
|
|
|
|
|
return centroA.localeCompare(centroB);
|
2025-11-19 20:31:54 +00:00
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
sortedCentros.forEach(([centro, centroItems]) => {
|
|
|
|
|
const totalCentro = centroItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se centro de custo contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquidoCC = centroItems.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculoCC = temFaturamentoLiquidoCC ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha do centro de custo (Level 2) - sempre expandida
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "centro_custo",
|
|
|
|
|
level: 2,
|
|
|
|
|
entidade,
|
|
|
|
|
direto_indireto: diretoIndireto,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
codigo_centro_custo: centroItems[0].codigo_centro_custo,
|
|
|
|
|
total: totalCentro,
|
|
|
|
|
isExpanded: true,
|
|
|
|
|
valoresPorMes: valoresCentroPorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresCentroPorMes, codgrupoParaCalculoCC),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalCentro, codgrupoParaCalculoCC),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Agrupar por conta dentro do centro de custo
|
|
|
|
|
const contas = centroItems.reduce((acc, item) => {
|
|
|
|
|
const conta = item.conta || "";
|
|
|
|
|
if (!conta) return acc;
|
|
|
|
|
if (!acc[conta]) {
|
|
|
|
|
acc[conta] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[conta].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<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
|
|
|
|
|
);
|
|
|
|
|
const valoresContaPorMes = calcularValoresPorMes(contaItems);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se conta contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquidoConta = contaItems.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculoConta = temFaturamentoLiquidoConta ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha da conta (Level 3)
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "conta",
|
|
|
|
|
level: 3,
|
|
|
|
|
entidade,
|
|
|
|
|
direto_indireto: diretoIndireto,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
conta,
|
|
|
|
|
codigo_conta: contaItems[0].codigo_conta,
|
|
|
|
|
codigo_centro_custo: centroItems[0].codigo_centro_custo,
|
|
|
|
|
total: totalConta,
|
|
|
|
|
valoresPorMes: valoresContaPorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupoParaCalculoConta),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalConta, codgrupoParaCalculoConta),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
2025-11-19 20:31:54 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleExpandAll = useCallback(() => {
|
|
|
|
|
if (isAllExpanded) {
|
|
|
|
|
// Recolher tudo
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
setExpandedEntidades(new Set());
|
|
|
|
|
setExpandedDiretoIndireto(new Set());
|
|
|
|
|
setExpandedCentros(new Set());
|
|
|
|
|
setIsAllExpanded(false);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Expandir todos os níveis da nova hierarquia [entidade, direto/indireto, cc, conta]
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
const todasEntidades = [...new Set(data.map(item => item.entidades).filter((e): e is string => Boolean(e)))];
|
|
|
|
|
const todosDiretoIndireto = [...new Set(
|
|
|
|
|
data.map(item => `${item.entidades}-${item.subgrupo}`).filter((e): e is string => Boolean(e))
|
|
|
|
|
)];
|
|
|
|
|
const todosCentros = [...new Set(
|
|
|
|
|
data.map(item => `${item.entidades}-${item.subgrupo}-${item.centro_custo}`).filter((e): e is string => Boolean(e))
|
|
|
|
|
)];
|
|
|
|
|
|
|
|
|
|
setExpandedEntidades(new Set(todasEntidades));
|
|
|
|
|
setExpandedDiretoIndireto(new Set(todosDiretoIndireto));
|
2025-11-19 20:31:54 +00:00
|
|
|
setExpandedCentros(new Set(todosCentros));
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
setIsAllExpanded(true);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [isAllExpanded, data]);
|
|
|
|
|
|
|
|
|
|
// Função para recalcular grupos calculados baseado apenas nos dados filtrados
|
|
|
|
|
const recalcularGruposCalculados = (dadosFiltrados: DREItem[]): DREItem[] => {
|
|
|
|
|
const gruposCalculados: DREItem[] = [];
|
|
|
|
|
|
|
|
|
|
// Agrupar dados por mês para cálculos
|
|
|
|
|
const dadosPorMes = dadosFiltrados.reduce((acc, item) => {
|
|
|
|
|
const mes = item.data_competencia;
|
|
|
|
|
if (!acc[mes]) acc[mes] = [];
|
|
|
|
|
acc[mes].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<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([]);
|
|
|
|
|
setEntidadesSelecionadas([]);
|
|
|
|
|
|
|
|
|
|
// Limpar filtros de busca
|
|
|
|
|
setFiltroCentroCusto("");
|
|
|
|
|
setFiltroConta("");
|
|
|
|
|
setFiltroEntidade("");
|
|
|
|
|
|
|
|
|
|
// Limpar dados da tabela
|
|
|
|
|
setData([]);
|
|
|
|
|
setDadosFiltrados([]);
|
|
|
|
|
setFiltrosAplicados(false);
|
|
|
|
|
setMesesDisponiveis([]);
|
|
|
|
|
setIsAllExpanded(false);
|
|
|
|
|
setOrdemHierarquiaContasPrimeiro(false);
|
|
|
|
|
|
|
|
|
|
// Fechar o sheet de filtros
|
|
|
|
|
setIsFilterOpen(false);
|
|
|
|
|
|
|
|
|
|
// Recarregar opções e selecionar todos novamente
|
|
|
|
|
carregarPeriodosDisponiveis();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const aplicarFiltros = async () => {
|
|
|
|
|
// Fechar o Sheet primeiro
|
|
|
|
|
setIsFilterOpen(false);
|
|
|
|
|
|
|
|
|
|
// Aguardar um pouco para a animação de fechamento
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
// Carregar dados da API
|
|
|
|
|
const response = await fetch("/api/dre-entidade-oracle");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro HTTP: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dadosCompletos = await response.json();
|
|
|
|
|
|
|
|
|
|
// Aplicar filtros nos dados
|
|
|
|
|
let dadosFiltrados = dadosCompletos;
|
|
|
|
|
|
|
|
|
|
// Filtro por período
|
|
|
|
|
if (filtros.periodoDe && filtros.periodoAte) {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
|
|
|
|
|
const dataItem = item.data_competencia;
|
|
|
|
|
return dataItem >= filtros.periodoDe && dataItem <= filtros.periodoAte;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por grupo
|
|
|
|
|
if (filtros.grupo !== "Todos") {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
item.grupo === filtros.grupo
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por subgrupo
|
|
|
|
|
if (filtros.subgrupo !== "Todos") {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
item.subgrupo === filtros.subgrupo
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 21:08:03 +00:00
|
|
|
// Filtro por centro de custo (multi-seleção) - USAR APENAS CÓDIGO
|
|
|
|
|
// IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO")
|
|
|
|
|
// Sempre incluir centro de custo 999.998 mesmo que não esteja na lista de selecionados
|
|
|
|
|
const centroCusto999998 = dadosCompletos.find((d: DREItem) => d.codigo_centro_custo === "999.998")?.centro_custo;
|
|
|
|
|
const centrosCustoParaFiltrar = centroCusto999998 && !centrosCustoSelecionados.includes(centroCusto999998)
|
|
|
|
|
? [...centrosCustoSelecionados, centroCusto999998]
|
|
|
|
|
: centrosCustoSelecionados;
|
|
|
|
|
|
|
|
|
|
if (centrosCustoParaFiltrar.length > 0) {
|
|
|
|
|
// Criar conjunto de códigos esperados dos centros selecionados - APENAS CÓDIGOS
|
|
|
|
|
const codigosEsperados = new Set<string>();
|
|
|
|
|
|
|
|
|
|
// Sempre incluir código 999.998
|
|
|
|
|
codigosEsperados.add("999.998");
|
|
|
|
|
|
|
|
|
|
centrosCustoParaFiltrar.forEach(centro => {
|
|
|
|
|
// Buscar o código no mapeamento primeiro
|
|
|
|
|
const codigoCentro = codigosCentrosCusto[centro];
|
|
|
|
|
if (codigoCentro) {
|
|
|
|
|
codigosEsperados.add(codigoCentro);
|
|
|
|
|
} else {
|
|
|
|
|
// Se não encontrar no mapeamento, tentar buscar nos dados carregados
|
|
|
|
|
const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro);
|
|
|
|
|
if (item?.codigo_centro_custo) {
|
|
|
|
|
codigosEsperados.add(item.codigo_centro_custo);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 21:08:03 +00:00
|
|
|
// Filtro por conta (multi-seleção)
|
|
|
|
|
// IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO")
|
|
|
|
|
// Sempre incluir conta 199 mesmo que não esteja na lista de selecionadas
|
|
|
|
|
const conta199 = dadosCompletos.find((d: DREItem) => d.codigo_conta?.toString() === "199")?.conta;
|
|
|
|
|
const contasParaFiltrar = conta199 && !contasSelecionadas.includes(conta199)
|
|
|
|
|
? [...contasSelecionadas, conta199]
|
|
|
|
|
: contasSelecionadas;
|
|
|
|
|
|
|
|
|
|
if (contasParaFiltrar.length > 0) {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
|
|
|
|
|
// Preservar grupos calculados
|
|
|
|
|
if (item.centro_custo === "CALCULADO" || item.isCalculado === true) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Para outros itens, verificar se a conta está selecionada (incluindo conta 199)
|
|
|
|
|
return contasParaFiltrar.includes(item.conta);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 21:08:03 +00:00
|
|
|
// Filtro por entidades (multi-seleção)
|
|
|
|
|
// IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO")
|
|
|
|
|
// Sempre incluir "Faturamento líquido" mesmo que não esteja na lista de selecionadas
|
|
|
|
|
const entidadeFaturamentoLiquido = dadosCompletos.find((d: DREItem) =>
|
|
|
|
|
d.entidades && (
|
|
|
|
|
d.entidades.toLowerCase().includes('faturamento') &&
|
|
|
|
|
(d.entidades.toLowerCase().includes('líquido') || d.entidades.toLowerCase().includes('liquido'))
|
|
|
|
|
)
|
|
|
|
|
)?.entidades;
|
|
|
|
|
|
|
|
|
|
const entidadesParaFiltrar = entidadeFaturamentoLiquido
|
|
|
|
|
? [...entidadesSelecionadas, entidadeFaturamentoLiquido]
|
|
|
|
|
: entidadesSelecionadas;
|
|
|
|
|
|
|
|
|
|
if (entidadesParaFiltrar.length > 0) {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
|
|
|
|
|
// Preservar grupos calculados
|
|
|
|
|
if (item.centro_custo === "CALCULADO" || item.isCalculado === true) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Para outros itens, verificar se a entidade está selecionada (incluindo Faturamento líquido)
|
|
|
|
|
return item.entidades && entidadesParaFiltrar.includes(item.entidades);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('🏢 Filtro de entidades aplicado:', {
|
|
|
|
|
selecionadas: entidadesSelecionadas,
|
|
|
|
|
incluindoFaturamentoLiquido: entidadeFaturamentoLiquido,
|
|
|
|
|
totalFiltrado: dadosFiltrados.length,
|
|
|
|
|
entidadesEncontradas: [...new Set(dadosFiltrados.map((d: DREItem) => d.entidades).filter(Boolean))],
|
|
|
|
|
gruposCalculados: dadosFiltrados.filter((d: DREItem) => d.centro_custo === "CALCULADO" || d.isCalculado === true).length
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Função auxiliar para calcular o faturamento líquido por mês
|
|
|
|
|
const calcularFaturamentoLiquidoPorMes = (): Record<string, number> => {
|
|
|
|
|
const faturamentoLiquidoPorMes: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
mesesDisponiveis.forEach(mes => {
|
|
|
|
|
// Buscar itens com CODGRUPO = "00" (faturamento líquido)
|
|
|
|
|
const dadosMes = data.filter(item => item.data_competencia === mes);
|
|
|
|
|
|
|
|
|
|
const faturamentoLiquidoItems = dadosMes.filter(item => item.codgrupo === "00");
|
|
|
|
|
|
|
|
|
|
if (faturamentoLiquidoItems.length > 0) {
|
|
|
|
|
// Se encontrou itens com CODGRUPO = "00", somar seus valores
|
|
|
|
|
faturamentoLiquidoPorMes[mes] = faturamentoLiquidoItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// Se não encontrou, retornar 0
|
|
|
|
|
faturamentoLiquidoPorMes[mes] = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return faturamentoLiquidoPorMes;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função para calcular percentuais baseado no faturamento líquido
|
2025-11-17 13:13:32 +00:00
|
|
|
const calcularPercentuaisPorMes = (
|
|
|
|
|
valoresPorMes: Record<string, number>,
|
2025-11-19 20:12:58 +00:00
|
|
|
grupo: string // Usado para verificar se é faturamento líquido (CODGRUPO = "00")
|
2025-11-17 13:13:32 +00:00
|
|
|
): Record<string, number> => {
|
|
|
|
|
const percentuais: Record<string, number> = {};
|
2025-11-19 20:12:58 +00:00
|
|
|
const faturamentoLiquidoPorMes = calcularFaturamentoLiquidoPorMes();
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se é faturamento líquido (CODGRUPO = "00")
|
|
|
|
|
const isFaturamentoLiquido = grupo === "00" || grupo.includes("00") || grupo.includes("FATURAMENTO LÍQUIDO");
|
2025-11-17 13:13:32 +00:00
|
|
|
|
|
|
|
|
Object.keys(valoresPorMes).forEach((mes) => {
|
|
|
|
|
const valorAtual = valoresPorMes[mes];
|
2025-11-19 20:12:58 +00:00
|
|
|
const faturamentoLiquido = faturamentoLiquidoPorMes[mes] || 0;
|
|
|
|
|
|
|
|
|
|
if (isFaturamentoLiquido) {
|
|
|
|
|
// Se for faturamento líquido, sempre retornar 100%
|
|
|
|
|
percentuais[mes] = 100;
|
|
|
|
|
} else if (faturamentoLiquido !== 0) {
|
|
|
|
|
// Calcular percentual baseado no faturamento líquido
|
|
|
|
|
percentuais[mes] = (valorAtual / faturamentoLiquido) * 100;
|
2025-11-17 13:13:32 +00:00
|
|
|
} else {
|
|
|
|
|
percentuais[mes] = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return percentuais;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Função para calcular percentual do total baseado no faturamento líquido
|
2025-11-17 13:13:32 +00:00
|
|
|
const calcularPercentualTotal = (
|
|
|
|
|
total: number,
|
2025-11-19 20:12:58 +00:00
|
|
|
grupo: string // Usado para verificar se é faturamento líquido (CODGRUPO = "00")
|
2025-11-17 13:13:32 +00:00
|
|
|
): number => {
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se é faturamento líquido
|
|
|
|
|
const isFaturamentoLiquido = grupo === "00" || grupo.includes("00") || grupo.includes("FATURAMENTO LÍQUIDO");
|
|
|
|
|
|
|
|
|
|
if (isFaturamentoLiquido) {
|
|
|
|
|
// Se for faturamento líquido, sempre retornar 100%
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calcular o total do faturamento líquido
|
|
|
|
|
const faturamentoLiquidoPorMes = calcularFaturamentoLiquidoPorMes();
|
|
|
|
|
const totalFaturamentoLiquido = Object.values(faturamentoLiquidoPorMes).reduce(
|
|
|
|
|
(sum, valor) => sum + valor,
|
2025-11-17 13:13:32 +00:00
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
if (totalFaturamentoLiquido !== 0) {
|
|
|
|
|
return (total / totalFaturamentoLiquido) * 100;
|
2025-11-17 13:13:32 +00:00
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildHierarchicalData = (): HierarchicalRow[] => {
|
|
|
|
|
const rows: HierarchicalRow[] = [];
|
|
|
|
|
|
|
|
|
|
// Nova hierarquia: [entidade, direto/indireto, cc, conta]
|
2025-11-19 20:31:54 +00:00
|
|
|
// Agrupar primeiro por CODGRUPO, depois por entidade
|
|
|
|
|
const gruposPorCodigo = data.reduce((acc, item) => {
|
|
|
|
|
const codgrupo = item.codgrupo || "";
|
|
|
|
|
if (!codgrupo) return acc;
|
|
|
|
|
if (!acc[codgrupo]) {
|
|
|
|
|
acc[codgrupo] = {};
|
|
|
|
|
}
|
2025-11-17 13:13:32 +00:00
|
|
|
const entidade = item.entidades || "";
|
|
|
|
|
if (!entidade) return acc;
|
2025-11-19 20:31:54 +00:00
|
|
|
if (!acc[codgrupo][entidade]) {
|
|
|
|
|
acc[codgrupo][entidade] = [];
|
2025-11-17 13:13:32 +00:00
|
|
|
}
|
2025-11-19 20:31:54 +00:00
|
|
|
acc[codgrupo][entidade].push(item);
|
2025-11-17 13:13:32 +00:00
|
|
|
return acc;
|
2025-11-19 20:31:54 +00:00
|
|
|
}, {} as Record<string, Record<string, DREItem[]>>);
|
|
|
|
|
|
|
|
|
|
// Ordenar por CODGRUPO (numericamente), depois por entidade (alfabeticamente)
|
|
|
|
|
const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => {
|
|
|
|
|
// Ordenar numericamente por CODGRUPO
|
|
|
|
|
const numA = parseInt(codA) || 0;
|
|
|
|
|
const numB = parseInt(codB) || 0;
|
|
|
|
|
if (numA !== numB) {
|
|
|
|
|
return numA - numB;
|
|
|
|
|
}
|
|
|
|
|
// Se não for numérico, ordenar alfabeticamente
|
|
|
|
|
return codA.localeCompare(codB);
|
|
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
sortedGrupos.forEach(([codgrupo, entidades]) => {
|
|
|
|
|
// Ordenar entidades dentro do grupo alfabeticamente
|
|
|
|
|
const sortedEntidades = Object.entries(entidades).sort(([entA], [entB]) =>
|
|
|
|
|
entA.localeCompare(entB)
|
|
|
|
|
);
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
sortedEntidades.forEach(([entidade, items]) => {
|
2025-11-17 13:13:32 +00:00
|
|
|
// Calcular total da entidade
|
|
|
|
|
const totalEntidade = items.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresEntidadePorMes = calcularValoresPorMes(items);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se a entidade contém faturamento líquido (CODGRUPO = "00")
|
|
|
|
|
const temFaturamentoLiquido = items.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculo = temFaturamentoLiquido ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha da entidade (Level 0)
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "entidade",
|
|
|
|
|
level: 0,
|
|
|
|
|
entidade,
|
|
|
|
|
total: totalEntidade,
|
|
|
|
|
isExpanded: expandedEntidades.has(entidade),
|
|
|
|
|
valoresPorMes: valoresEntidadePorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresEntidadePorMes, codgrupoParaCalculo),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalEntidade, codgrupoParaCalculo),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedEntidades.has(entidade)) {
|
|
|
|
|
// Agrupar por direto/indireto (SUBGRUPO) dentro da entidade
|
|
|
|
|
const diretoIndireto = items.reduce((acc, item) => {
|
|
|
|
|
const subgrupo = item.subgrupo || ""; // SUBGRUPO contém "DIRETO" ou "INDIRETO"
|
|
|
|
|
if (!subgrupo) return acc;
|
|
|
|
|
if (!acc[subgrupo]) {
|
|
|
|
|
acc[subgrupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[subgrupo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Ordenar: DIRETO primeiro, depois INDIRETO, depois outros
|
|
|
|
|
const sortedDiretoIndireto = Object.entries(diretoIndireto).sort(([a], [b]) => {
|
|
|
|
|
if (a.toUpperCase() === "DIRETO" && b.toUpperCase() !== "DIRETO") return -1;
|
|
|
|
|
if (a.toUpperCase() !== "DIRETO" && b.toUpperCase() === "DIRETO") return 1;
|
|
|
|
|
if (a.toUpperCase() === "INDIRETO" && b.toUpperCase() !== "INDIRETO") return -1;
|
|
|
|
|
if (a.toUpperCase() !== "INDIRETO" && b.toUpperCase() === "INDIRETO") return 1;
|
|
|
|
|
return a.localeCompare(b);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedDiretoIndireto.forEach(([diretoIndireto, diretoIndiretoItems]) => {
|
|
|
|
|
const totalDiretoIndireto = diretoIndiretoItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresDiretoIndiretoPorMes = calcularValoresPorMes(diretoIndiretoItems);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se direto/indireto contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquidoDI = diretoIndiretoItems.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculoDI = temFaturamentoLiquidoDI ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha direto/indireto (Level 1)
|
|
|
|
|
const chaveDiretoIndireto = `${entidade}-${diretoIndireto}`;
|
|
|
|
|
rows.push({
|
|
|
|
|
type: "direto_indireto",
|
|
|
|
|
level: 1,
|
|
|
|
|
entidade,
|
|
|
|
|
direto_indireto: diretoIndireto,
|
|
|
|
|
total: totalDiretoIndireto,
|
|
|
|
|
isExpanded: expandedDiretoIndireto.has(chaveDiretoIndireto),
|
|
|
|
|
valoresPorMes: valoresDiretoIndiretoPorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresDiretoIndiretoPorMes, codgrupoParaCalculoDI),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalDiretoIndireto, codgrupoParaCalculoDI),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedDiretoIndireto.has(chaveDiretoIndireto)) {
|
|
|
|
|
// Agrupar por centro de custo dentro de direto/indireto
|
|
|
|
|
const centros = diretoIndiretoItems.reduce((acc, item) => {
|
|
|
|
|
const centro = item.centro_custo || "";
|
|
|
|
|
if (!centro) return acc;
|
|
|
|
|
if (!acc[centro]) {
|
|
|
|
|
acc[centro] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[centro].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
// 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);
|
|
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
sortedCentros.forEach(([centro, centroItems]) => {
|
|
|
|
|
const totalCentro = centroItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
2025-11-17 13:13:32 +00:00
|
|
|
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se centro de custo contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquidoCC = centroItems.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculoCC = temFaturamentoLiquidoCC ? "00" : "";
|
|
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
// Linha do centro de custo (Level 2)
|
2025-11-17 13:13:32 +00:00
|
|
|
const chaveCentro = `${entidade}-${diretoIndireto}-${centro}`;
|
2025-11-19 20:31:54 +00:00
|
|
|
rows.push({
|
|
|
|
|
type: "centro_custo",
|
|
|
|
|
level: 2,
|
2025-11-17 13:13:32 +00:00
|
|
|
entidade,
|
|
|
|
|
direto_indireto: diretoIndireto,
|
2025-11-19 20:31:54 +00:00
|
|
|
centro_custo: centro,
|
|
|
|
|
codigo_centro_custo: centroItems[0].codigo_centro_custo,
|
|
|
|
|
total: totalCentro,
|
2025-11-17 13:13:32 +00:00
|
|
|
isExpanded: expandedCentros.has(chaveCentro),
|
2025-11-19 20:31:54 +00:00
|
|
|
valoresPorMes: valoresCentroPorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresCentroPorMes, codgrupoParaCalculoCC),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalCentro, codgrupoParaCalculoCC),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedCentros.has(chaveCentro)) {
|
|
|
|
|
// Agrupar por conta dentro do centro de custo
|
|
|
|
|
const contas = centroItems.reduce((acc, item) => {
|
|
|
|
|
const conta = item.conta || "";
|
|
|
|
|
if (!conta) return acc;
|
|
|
|
|
if (!acc[conta]) {
|
|
|
|
|
acc[conta] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[conta].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
2025-11-19 20:31:54 +00:00
|
|
|
// 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);
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortedContas.forEach(([conta, contaItems]) => {
|
|
|
|
|
const totalConta = contaItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
const valoresContaPorMes = calcularValoresPorMes(contaItems);
|
|
|
|
|
|
2025-11-19 20:12:58 +00:00
|
|
|
// Verificar se conta contém faturamento líquido
|
|
|
|
|
const temFaturamentoLiquidoConta = contaItems.some(item => item.codgrupo === "00");
|
|
|
|
|
const codgrupoParaCalculoConta = temFaturamentoLiquidoConta ? "00" : "";
|
|
|
|
|
|
2025-11-17 13:13:32 +00:00
|
|
|
// Linha da conta (Level 3)
|
|
|
|
|
rows.push({
|
2025-11-19 20:31:54 +00:00
|
|
|
type: "conta",
|
2025-11-17 13:13:32 +00:00
|
|
|
level: 3,
|
|
|
|
|
entidade,
|
|
|
|
|
direto_indireto: diretoIndireto,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
conta,
|
|
|
|
|
codigo_conta: contaItems[0].codigo_conta,
|
|
|
|
|
codigo_centro_custo: centroItems[0].codigo_centro_custo,
|
|
|
|
|
total: totalConta,
|
|
|
|
|
valoresPorMes: valoresContaPorMes,
|
2025-11-19 20:12:58 +00:00
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupoParaCalculoConta),
|
|
|
|
|
percentualTotal: calcularPercentualTotal(totalConta, codgrupoParaCalculoConta),
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-19 20:31:54 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-11-17 13:13:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRowStyle = (row: HierarchicalRow) => {
|
|
|
|
|
const baseStyle =
|
|
|
|
|
"transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30";
|
|
|
|
|
|
|
|
|
|
// Criar identificador único para a linha
|
|
|
|
|
const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${
|
|
|
|
|
row.centro_custo || ""
|
|
|
|
|
}-${row.codigo_conta || ""}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
|
|
|
|
|
let style = baseStyle;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
style +=
|
|
|
|
|
" bg-gradient-to-r from-green-100 to-emerald-100 border-l-4 border-green-500 shadow-lg";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (row.type) {
|
|
|
|
|
case "entidade":
|
|
|
|
|
return `${style} bg-gradient-to-r from-blue-50/20 to-indigo-50/20 font-bold text-gray-900 border-b-2 border-blue-200`;
|
|
|
|
|
case "direto_indireto":
|
|
|
|
|
return `${style} bg-gradient-to-r from-gray-50/30 to-blue-50/20 font-semibold text-gray-800`;
|
|
|
|
|
case "centro_custo":
|
|
|
|
|
return `${style} bg-gradient-to-r from-gray-50/20 to-gray-100/10 font-medium text-gray-700`;
|
|
|
|
|
case "conta":
|
|
|
|
|
return `${style} bg-white font-normal text-gray-600`;
|
|
|
|
|
default:
|
|
|
|
|
return style;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função para obter o background da célula fixa baseado no tipo de linha
|
|
|
|
|
const getFixedCellBackground = (row: HierarchicalRow): string => {
|
|
|
|
|
const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${
|
|
|
|
|
row.centro_custo || ""
|
|
|
|
|
}-${row.codigo_conta || ""}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
return "bg-gradient-to-r from-green-100 to-emerald-100";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (row.type) {
|
|
|
|
|
case "entidade":
|
|
|
|
|
return "bg-gradient-to-r from-blue-50 to-indigo-50";
|
|
|
|
|
case "direto_indireto":
|
|
|
|
|
return "bg-gradient-to-r from-gray-50 to-blue-50";
|
|
|
|
|
case "centro_custo":
|
|
|
|
|
return "bg-gradient-to-r from-gray-50 to-gray-100";
|
|
|
|
|
case "conta":
|
|
|
|
|
return "bg-white";
|
|
|
|
|
default:
|
|
|
|
|
return "bg-white";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getIndentStyle = (level: number) => {
|
|
|
|
|
return { paddingLeft: `${level * 20}px` };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderCellContent = (row: HierarchicalRow) => {
|
|
|
|
|
switch (row.type) {
|
|
|
|
|
case "entidade":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleEntidade(row.entidade!)}
|
|
|
|
|
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.entidade}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
case "direto_indireto":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleDiretoIndireto(`${row.entidade}-${row.direto_indireto}`)}
|
2025-11-19 20:31:54 +00:00
|
|
|
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" />
|
|
|
|
|
)}
|
2025-11-17 13:13:32 +00:00
|
|
|
</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-semibold text-gray-800">
|
|
|
|
|
{row.direto_indireto}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
case "centro_custo":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleCentro(`${row.entidade}-${row.direto_indireto}-${row.centro_custo}`)}
|
2025-11-19 20:31:54 +00:00
|
|
|
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" />
|
|
|
|
|
)}
|
2025-11-17 13:13:32 +00:00
|
|
|
</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"
|
|
|
|
|
>
|
2025-11-19 20:31:54 +00:00
|
|
|
<div className="flex items-center gap-2">
|
2025-11-17 13:13:32 +00:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
case "conta":
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
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] flex flex-col">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>Filtros</SheetTitle>
|
|
|
|
|
<SheetDescription>
|
|
|
|
|
Ajuste os critérios e clique em Pesquisar para atualizar a visão.
|
|
|
|
|
</SheetDescription>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto pr-2">
|
|
|
|
|
<div className="grid 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>*/}
|
|
|
|
|
|
|
|
|
|
{/* Entidades */}
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Label htmlFor="entidades">ENTIDADES</Label>
|
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={selecionarTodasEntidades}
|
|
|
|
|
className="text-xs h-5 px-2"
|
|
|
|
|
>
|
|
|
|
|
Todas
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={limparEntidades}
|
|
|
|
|
className="text-xs h-5 px-2"
|
|
|
|
|
>
|
|
|
|
|
Limpar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Input de filtro para Entidades */}
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Filtrar entidades..."
|
|
|
|
|
value={filtroEntidade}
|
|
|
|
|
onChange={(e) => setFiltroEntidade(e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
/>
|
|
|
|
|
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
|
|
|
|
|
{opcoesEntidades
|
|
|
|
|
.filter(entidade => {
|
|
|
|
|
if (!filtroEntidade) return true;
|
|
|
|
|
const termo = filtroEntidade.toLowerCase();
|
|
|
|
|
return entidade.toLowerCase().includes(termo);
|
|
|
|
|
})
|
|
|
|
|
.map(entidade => (
|
|
|
|
|
<div key={entidade} className="flex items-center space-x-1">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id={`entidade-${entidade}`}
|
|
|
|
|
checked={entidadesSelecionadas.includes(entidade)}
|
|
|
|
|
onCheckedChange={() => toggleEntidadeFiltro(entidade)}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor={`entidade-${entidade}`}
|
|
|
|
|
className="text-sm font-normal cursor-pointer flex-1"
|
|
|
|
|
>
|
|
|
|
|
{entidade}
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{entidadesSelecionadas.length > 0 && (
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
{entidadesSelecionadas.length} entidade(s) selecionada(s)
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<SheetFooter className="flex gap-2 mt-4 border-t pt-4">
|
|
|
|
|
<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 overflow-hidden">
|
|
|
|
|
{/* Scroll Container - Apenas um container com scroll */}
|
|
|
|
|
<div
|
|
|
|
|
className="overflow-x-auto overflow-y-auto max-h-[500px]"
|
|
|
|
|
style={{ scrollbarWidth: 'thin' }}
|
|
|
|
|
>
|
|
|
|
|
{/* Table */}
|
|
|
|
|
<table
|
|
|
|
|
className="w-full border-collapse"
|
|
|
|
|
style={{ minWidth: 'max-content' }}
|
|
|
|
|
>
|
|
|
|
|
{/* Table Header */}
|
|
|
|
|
<thead className="sticky top-0 z-10 bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
<tr className="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] bg-gradient-to-r from-blue-50 to-indigo-50 sticky left-0 z-20 shadow-[2px_0_4px_rgba(0,0,0,0.1)]">
|
|
|
|
|
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] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
{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] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
%
|
|
|
|
|
</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] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
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] bg-gradient-to-r from-blue-50 to-indigo-50">
|
|
|
|
|
%
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
|
|
{/* Table Body */}
|
|
|
|
|
<tbody>
|
|
|
|
|
{hierarchicalData.map((row, index) => {
|
|
|
|
|
const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${row.centro_custo || ""}-${row.codigo_conta || ""}`;
|
|
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
return (
|
|
|
|
|
<tr
|
|
|
|
|
key={index}
|
|
|
|
|
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 sticky left-0 z-10 ${getFixedCellBackground(row)} shadow-[2px_0_4px_rgba(0,0,0,0.05)] cursor-pointer ${isSelected ? 'border-l-4 border-green-500' : ''}`}
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
|
|
|
|
>
|
|
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Componente Analítico - Sempre renderizado para evitar violação das Rules of Hooks */}
|
|
|
|
|
<AnaliticoComponent filtros={analiticoFiltros} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|