vendaweb-api/src/app/dre-filial/teste.tsx

2371 lines
84 KiB
TypeScript
Raw Normal View History

'use client';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import {
ChevronDown,
ChevronRight,
Download,
Filter,
LoaderPinwheel,
Maximize2,
Minimize2,
} from 'lucide-react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import * as XLSX from 'xlsx';
import AnaliticoComponent from './analitico';
interface DREItem {
codfilial: string;
data_competencia: string;
data_cai: string;
grupo: string;
codigo_grupo: string;
codigo_conta: number;
conta: string;
valor: string;
codgrupo?: string;
tipo?: string;
filial?: string;
}
interface HierarchicalRow {
type: 'grupo' | 'conta' | 'calculado';
level: number;
grupo?: string;
codigo_grupo?: string;
conta?: string;
codigo_conta?: number;
total?: number;
isExpanded?: boolean;
valoresPorMes?: Record<string, number>;
2025-12-09 20:41:58 +00:00
valoresPorMesPorFilial?: Record<string, Record<string, number>>; // mes -> filial -> valor
percentuaisPorMes?: Record<string, number>;
2025-12-09 20:41:58 +00:00
percentuaisPorMesPorFilial?: Record<string, Record<string, number>>; // mes -> filial -> percentual
percentualTotal?: number;
2025-12-09 19:19:55 +00:00
isCalculado?: boolean;
}
// Componente memoizado para linhas da tabela
const TableRow = memo(
({
row,
index,
handleRowClick,
getRowStyle,
getIndentStyle,
renderCellContent,
mesesDisponiveis,
opcoesFiliais,
filiaisSelecionadas,
filtrosAplicados,
formatCurrency,
formatCurrencyWithColor,
getFixedCellBackground,
}: {
row: HierarchicalRow;
index: number;
handleRowClick: (
row: HierarchicalRow,
mes?: string,
filial?: string
) => void;
getRowStyle: (row: HierarchicalRow) => string;
getIndentStyle: (level: number) => React.CSSProperties;
renderCellContent: (row: HierarchicalRow) => React.ReactNode;
mesesDisponiveis: string[];
opcoesFiliais: string[];
filiaisSelecionadas: string[];
filtrosAplicados: boolean;
formatCurrency: (value: number) => string;
formatCurrencyWithColor: (value: number) => {
formatted: string;
isNegative: boolean;
};
getFixedCellBackground: (row: HierarchicalRow) => string;
}) => {
return (
<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 sticky left-0 z-10 ${getFixedCellBackground(
row
)} shadow-[2px_0_4px_rgba(0,0,0,0.05)] cursor-pointer`}
onClick={() => handleRowClick(row)}
>
<div style={getIndentStyle(row.level)}>{renderCellContent(row)}</div>
</td>
{/* Colunas de valores por mês e por filial - cada filial tem suas próprias colunas */}
{mesesDisponiveis.map((mes) => {
const filiaisParaMes =
filtrosAplicados && filiaisSelecionadas.length > 0
? filiaisSelecionadas
: opcoesFiliais.length > 0
? opcoesFiliais
: [''];
return (
<React.Fragment key={mes}>
{/* Colunas de filiais para este mês */}
{filiaisParaMes.map((filial: string) => {
// Só exibir se a filial estiver selecionada ou se não houver filtros aplicados
if (
filtrosAplicados &&
filiaisSelecionadas.length > 0 &&
!filiaisSelecionadas.includes(filial)
) {
return null;
}
return (
<React.Fragment key={`${mes}-${filial || 'default'}`}>
<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, filial)}
title={
filial &&
row.valoresPorMesPorFilial?.[mes]?.[filial] !==
undefined
? formatCurrency(
row.valoresPorMesPorFilial[mes][filial]
)
: row.valoresPorMes?.[mes] !== undefined
? formatCurrency(row.valoresPorMes[mes])
: '-'
2025-12-09 20:41:58 +00:00
}
>
{filial &&
row.valoresPorMesPorFilial?.[mes]?.[filial] !==
undefined &&
row.valoresPorMesPorFilial[mes][filial] !== 0 ? (
(() => {
const valor = row.valoresPorMesPorFilial[mes][filial];
const { formatted, isNegative } =
formatCurrencyWithColor(valor);
return (
<span
className={
isNegative
? 'text-red-600 font-bold'
: 'text-gray-900'
}
>
{formatted}
</span>
);
})()
) : !filial && row.valoresPorMes?.[mes] !== undefined ? (
(() => {
const { formatted, isNegative } =
formatCurrencyWithColor(row.valoresPorMes[mes]);
return (
<span
className={
isNegative
? 'text-red-600 font-bold'
: 'text-gray-900'
}
>
{formatted}
</span>
);
})()
) : (
<span className="text-gray-400">-</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, filial)}
title={
filial &&
row.percentuaisPorMesPorFilial?.[mes]?.[filial] !==
undefined
? `${row.percentuaisPorMesPorFilial[mes][
filial
].toFixed(1)}%`
: row.percentuaisPorMes?.[mes] !== undefined
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
: '-'
}
>
{filial &&
row.percentuaisPorMesPorFilial?.[mes]?.[filial] !==
undefined &&
row.percentuaisPorMesPorFilial[mes][filial] !== 0 ? (
`${row.percentuaisPorMesPorFilial[mes][filial].toFixed(
1
)}%`
) : !filial &&
row.percentuaisPorMes?.[mes] !== undefined ? (
`${row.percentuaisPorMes[mes].toFixed(1)}%`
) : (
<span className="text-gray-400">-</span>
)}
</td>
</React.Fragment>
);
})}
{/* Colunas de totalizador para este mês */}
<td
className="px-2 py-1 text-right font-bold cursor-pointer hover:bg-green-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[120px] min-w-[120px] bg-green-50/30"
onClick={() => handleRowClick(row, mes)}
title={
row.valoresPorMes?.[mes] !== undefined
? formatCurrency(row.valoresPorMes[mes])
: '-'
}
>
{row.valoresPorMes?.[mes] !== undefined ? (
(() => {
const { formatted, isNegative } = formatCurrencyWithColor(
row.valoresPorMes[mes]
);
return (
<span
className={
isNegative
? 'text-red-600 font-bold'
: 'text-green-700 font-bold'
}
>
{formatted}
</span>
);
})()
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td
className="px-2 py-1 text-center font-bold cursor-pointer hover:bg-green-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[100px] min-w-[100px] bg-green-50/30"
onClick={() => handleRowClick(row, mes)}
title={
row.percentuaisPorMes?.[mes] !== undefined
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
: '-'
}
>
{row.percentuaisPorMes?.[mes] !== undefined ? (
`${row.percentuaisPorMes[mes].toFixed(1)}%`
) : (
<span className="text-gray-400">-</span>
)}
</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() {
// Função para ordenar filiais numericamente (1, 2, 3...) ao invés de alfabeticamente (1, 10, 11...)
const ordenarFiliaisNumericamente = (filiais: string[]): string[] => {
return [...filiais].sort((a, b) => {
// Tentar converter para número
const numA = parseInt(a, 10);
const numB = parseInt(b, 10);
// Se ambos são números válidos, ordenar numericamente
if (!isNaN(numA) && !isNaN(numB)) {
return numA - numB;
}
// Se apenas A é número, vem primeiro
if (!isNaN(numA)) {
return -1;
}
// Se apenas B é número, vem primeiro
if (!isNaN(numB)) {
return 1;
}
// Se nenhum é número, ordenar alfabeticamente
return a.localeCompare(b);
});
};
const [data, setData] = useState<DREItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [expandedGrupos, setExpandedGrupos] = useState<Set<string>>(new Set());
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
// Estados para filtros
const [filtros, setFiltros] = useState({
periodoDe: '',
periodoAte: '',
filial: 'Todas',
});
// Estados para multi-seleção
const [filiaisSelecionadas, setFiliaisSelecionadas] = useState<string[]>([]);
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [dadosFiltrados, setDadosFiltrados] = useState<DREItem[]>([]);
const [filtrosAplicados, setFiltrosAplicados] = useState(false);
// Estados para opções dos filtros
const [opcoesGrupos, setOpcoesGrupos] = useState<string[]>([]);
const [opcoesFiliais, setOpcoesFiliais] = useState<string[]>([]);
// Estados para filtros de busca nos campos de seleção
const [filtroFilial, setFiltroFilial] = useState<string>('');
// Estados para analítico
const [analiticoFiltros, setAnaliticoFiltros] = useState({
dataInicio: '',
dataFim: '',
centroCusto: '',
codigoGrupo: '',
codigoSubgrupo: '',
codigoConta: '',
codFilial: '',
linhaSelecionada: '',
excluirCentroCusto: '',
excluirCodigoConta: '',
codigosCentrosCustoSelecionados: '',
codigosContasSelecionadas: '',
});
const [linhaSelecionada, setLinhaSelecionada] = useState<string | null>(null);
const [isAllExpanded, setIsAllExpanded] = useState(false);
useEffect(() => {
// Carregar períodos disponíveis da API
carregarPeriodosDisponiveis();
}, []);
const carregarPeriodosDisponiveis = async () => {
try {
const response = await fetch('/api/dre-filial-oracle');
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const dadosCompletos = await response.json();
// Extrair períodos únicos dos dados
const periodosUnicos = [
...new Set(
dadosCompletos.map((item: DREItem) => item.data_competencia)
),
].sort() as string[];
setMesesDisponiveis(periodosUnicos);
// Extrair grupos únicos
const gruposUnicos = [
...new Set(dadosCompletos.map((item: DREItem) => item.grupo)),
].sort() as string[];
setOpcoesGrupos(gruposUnicos);
// Extrair filiais únicas
const filiaisUnicas = ordenarFiliaisNumericamente([
...new Set(
dadosCompletos
.map((item: DREItem) => item.filial || item.codfilial)
.filter(Boolean)
),
] as string[]);
setOpcoesFiliais(filiaisUnicas);
// Inicializar com todas as filiais selecionadas
setFiliaisSelecionadas(filiaisUnicas);
2025-12-09 14:46:20 +00:00
// Inicializar filtros de período com o ano corrente
const agora = new Date();
const anoAtual = agora.getFullYear();
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
const periodoAtual = `${anoAtual}-${mesAtual}`;
const primeiroMesAno = `${anoAtual}-01`;
2025-12-09 14:46:20 +00:00
// Verificar se os períodos existem nos dados disponíveis
const periodoDeValido = periodosUnicos.includes(primeiroMesAno)
? primeiroMesAno
: periodosUnicos[0] || primeiroMesAno;
const periodoAteValido = periodosUnicos.includes(periodoAtual)
? periodoAtual
: periodosUnicos[periodosUnicos.length - 1] || periodoAtual;
setFiltros((prev) => ({
2025-12-09 14:46:20 +00:00
...prev,
periodoDe: periodoDeValido,
periodoAte: periodoAteValido,
2025-12-09 14:46:20 +00:00
}));
// NÃO inicializar filtros do analítico - só serão definidos após clique em célula
} catch (error) {
console.error('Erro ao carregar períodos:', error);
}
};
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/dre-filial-oracle');
if (!response.ok) {
throw new Error(`Erro ao carregar dados: ${response.status}`);
}
const result = await response.json();
setData(result);
// Extrair meses únicos dos dados
const meses = [
...new Set(
result.map((item: DREItem) => {
return item.data_competencia;
})
),
].sort() as string[];
setMesesDisponiveis(meses);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setLoading(false);
}
};
2025-12-09 20:58:11 +00:00
const formatCurrency = React.useCallback((value: string | number) => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
return numValue.toLocaleString('pt-BR', {
style: 'currency',
currency: 'BRL',
});
2025-12-09 20:58:11 +00:00
}, []);
const formatCurrencyWithColor = React.useCallback(
(value: string | number) => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
const formatted = formatCurrency(value);
const isNegative = numValue < 0;
return { formatted, isNegative };
},
[formatCurrency]
);
// Função para lidar com clique nas linhas
const handleRowClick = React.useCallback(
(
row: HierarchicalRow,
mesSelecionado?: string,
filialSelecionada?: string
) => {
console.log('🖱️ Clique na linha:', row);
console.log('📅 Mês selecionado:', mesSelecionado);
console.log('🏢 Filial selecionada:', filialSelecionada);
// Linhas calculadas não devem abrir o componente analítico
if (row.type === 'calculado') {
console.log('⚠️ Linha calculada - não abre componente analítico');
return;
}
if (!data.length) {
console.log('⚠️ Sem dados disponíveis');
return;
}
// Pegar todas as datas disponíveis para definir o período
const datas = data.map((item) => item.data_competencia);
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7); // YYYY-MM
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7); // YYYY-MM
// Se um mês específico foi selecionado, usar apenas esse mês
const dataInicioFiltro = mesSelecionado || dataInicioStr;
const dataFimFiltro = mesSelecionado || dataFimStr;
// Determinar filtros baseado na hierarquia [grupo, conta]
let codigoGrupoFiltro = '';
let codigoContaFiltro = '';
if (row.type === 'grupo' || row.type === 'conta') {
// Buscar o CODGRUPO dos dados originais que correspondem a esta linha
const itemsCorrespondentes = data.filter((item: DREItem) => {
// Filtrar por período se um mês específico foi selecionado
if (mesSelecionado && item.data_competencia !== mesSelecionado) {
2025-12-10 12:30:21 +00:00
return false;
}
// Se é um totalizador (mesSelecionado presente mas filialSelecionada não), não filtrar por filial
// Caso contrário, filtrar por filial se especificada
if (filialSelecionada) {
const itemFilial = item.filial || item.codfilial || '';
if (itemFilial !== filialSelecionada) {
return false;
}
}
if (row.type === 'grupo') {
return (
item.codigo_grupo === row.codigo_grupo ||
item.codgrupo === row.codigo_grupo
);
} else if (row.type === 'conta') {
return (
(item.codigo_grupo === row.codigo_grupo ||
item.codgrupo === row.codigo_grupo) &&
item.codigo_conta === row.codigo_conta
);
}
return false;
2025-12-09 21:21:15 +00:00
});
// Pegar o CODGRUPO do primeiro item encontrado
if (itemsCorrespondentes.length > 0) {
const primeiroItem = itemsCorrespondentes[0];
codigoGrupoFiltro =
primeiroItem.codgrupo || primeiroItem.codigo_grupo || '';
2025-12-09 21:21:15 +00:00
}
}
// Filtrar por conta se for nível conta
if (row.type === 'conta') {
codigoContaFiltro = row.codigo_conta?.toString() || '';
}
// Determinar CODFILIAL baseado na filial selecionada
// Se é um totalizador (mesSelecionado presente mas filialSelecionada não), incluir todas as filiais selecionadas
let codFilialFiltro = '';
if (filialSelecionada) {
// Se a filial selecionada já é um código numérico, usar diretamente
// Caso contrário, buscar o CODFILIAL correspondente ao nome da filial nos dados
if (/^\d+$/.test(filialSelecionada)) {
codFilialFiltro = filialSelecionada;
2025-12-10 12:34:49 +00:00
} else {
// Buscar o CODFILIAL correspondente ao nome da filial nos dados
2025-12-10 12:34:49 +00:00
const itemComFilial = data.find((item: DREItem) => {
const itemFilial = item.filial || item.codfilial || '';
return itemFilial === filialSelecionada;
2025-12-10 12:34:49 +00:00
});
if (itemComFilial) {
codFilialFiltro = itemComFilial.codfilial || filialSelecionada;
} else {
// Se não encontrar, tentar usar o próprio valor como código
codFilialFiltro = filialSelecionada;
}
2025-12-10 12:34:49 +00:00
}
} else if (mesSelecionado) {
// Se mesSelecionado está presente mas filialSelecionada não, é um totalizador
// Incluir todas as filiais selecionadas (ou todas disponíveis se nenhuma estiver selecionada)
const filiaisParaTotalizador =
filiaisSelecionadas.length > 0 ? filiaisSelecionadas : opcoesFiliais;
// Converter nomes de filiais para códigos CODFILIAL
const codigosFiliais = filiaisParaTotalizador
.map((filial) => {
if (/^\d+$/.test(filial)) {
return filial;
} else {
const itemComFilial = data.find((item: DREItem) => {
const itemFilial = item.filial || item.codfilial || '';
return itemFilial === filial;
});
return itemComFilial?.codfilial || filial;
}
})
.filter(Boolean);
2025-12-09 21:21:15 +00:00
// Passar como string separada por vírgula para a API processar com IN
codFilialFiltro = codigosFiliais.join(',');
console.log(
'📊 Totalizador: incluindo todas as filiais selecionadas:',
codigosFiliais
);
}
const novosFiltros = {
dataInicio: dataInicioFiltro,
dataFim: dataFimFiltro,
centroCusto: '', // Não aplicável na hierarquia filial
codigoGrupo: codigoGrupoFiltro,
codigoSubgrupo: '', // Não aplicável na hierarquia filial
codigoConta: codigoContaFiltro,
codFilial: codFilialFiltro, // Vazio para totalizador = todas as filiais do mês
linhaSelecionada: row.grupo || row.conta || '',
excluirCentroCusto: '',
excluirCodigoConta: '',
codigosCentrosCustoSelecionados: '',
codigosContasSelecionadas: '',
};
console.log('🎯 Novos filtros para analítico:', novosFiltros);
console.log(
'📊 É totalizador?',
mesSelecionado && !filialSelecionada ? 'SIM' : 'NÃO'
);
setAnaliticoFiltros(novosFiltros);
},
[data, filiaisSelecionadas, opcoesFiliais]
);
const toggleGrupo = useCallback((codigoGrupo: string) => {
setExpandedGrupos((prev) => {
const newExpanded = new Set(prev);
if (newExpanded.has(codigoGrupo)) {
newExpanded.delete(codigoGrupo);
} else {
newExpanded.add(codigoGrupo);
}
return newExpanded;
});
}, []);
const toggleFilial = (filial: string) => {
setFiliaisSelecionadas((prev) => {
if (prev.includes(filial)) {
return prev.filter((f) => f !== filial);
} else {
return [...prev, filial];
}
});
};
const selecionarTodasFiliais = () => {
setFiliaisSelecionadas(opcoesFiliais);
};
const limparFiliais = () => {
setFiliaisSelecionadas([]);
};
// Função auxiliar para calcular valores por mês
const calcularValoresPorMes = React.useCallback(
(items: DREItem[]): Record<string, number> => {
const valoresPorMes: Record<string, number> = {};
mesesDisponiveis.forEach((mes) => {
valoresPorMes[mes] = 0;
});
items.forEach((item) => {
const anoMes = item.data_competencia;
if (anoMes && valoresPorMes[anoMes] !== undefined) {
valoresPorMes[anoMes] += parseFloat(item.valor);
}
});
return valoresPorMes;
},
[mesesDisponiveis]
);
2025-12-09 20:41:58 +00:00
// Função auxiliar para calcular valores por mês e por filial
const calcularValoresPorMesPorFilial = React.useCallback(
(items: DREItem[]): Record<string, Record<string, number>> => {
const valoresPorMesPorFilial: Record<string, Record<string, number>> = {};
2025-12-09 20:41:58 +00:00
// Extrair filiais únicas dos próprios items para evitar dependência de arrays externos
const filiaisDisponiveis = [
...new Set(
items.map((item) => item.filial || item.codfilial).filter(Boolean)
),
] as string[];
2025-12-09 20:41:58 +00:00
mesesDisponiveis.forEach((mes) => {
valoresPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
valoresPorMesPorFilial[mes][filial] = 0;
});
2025-12-09 20:41:58 +00:00
});
items.forEach((item) => {
const anoMes = item.data_competencia;
const filial = item.filial || item.codfilial || '';
if (anoMes && valoresPorMesPorFilial[anoMes] && filial) {
if (!valoresPorMesPorFilial[anoMes][filial]) {
valoresPorMesPorFilial[anoMes][filial] = 0;
}
valoresPorMesPorFilial[anoMes][filial] += parseFloat(item.valor);
2025-12-09 20:41:58 +00:00
}
});
2025-12-09 20:41:58 +00:00
return valoresPorMesPorFilial;
},
[mesesDisponiveis]
);
2025-12-09 20:58:11 +00:00
// Memoizar valores do grupo 01 por mês para evitar recálculos repetidos
const valoresGrupo01PorMesMemo = React.useMemo(() => {
const valores: Record<string, number> = {};
mesesDisponiveis.forEach((mes) => {
2025-12-09 20:58:11 +00:00
valores[mes] = data
.filter((item) => {
const codgrupo = item.codgrupo || item.codigo_grupo || '';
return codgrupo === '01' && item.data_competencia === mes;
2025-12-09 20:58:11 +00:00
})
.reduce((sum, item) => sum + parseFloat(item.valor), 0);
});
return valores;
}, [data, mesesDisponiveis]);
// Memoizar valores do grupo 01 por mês e por filial
const valoresGrupo01PorMesPorFilialMemo = React.useMemo(() => {
const valores: Record<string, Record<string, number>> = {};
2025-12-10 12:45:06 +00:00
// Extrair filiais únicas dos próprios dados para evitar dependência de arrays externos
const filiaisDisponiveis = [
...new Set(
data.map((item) => item.filial || item.codfilial).filter(Boolean)
),
] as string[];
mesesDisponiveis.forEach((mes) => {
2025-12-09 20:58:11 +00:00
valores[mes] = {};
filiaisDisponiveis.forEach((filial) => {
2025-12-09 20:58:11 +00:00
valores[mes][filial] = data
.filter((item) => {
const codgrupo = item.codgrupo || item.codigo_grupo || '';
const itemFilial = item.filial || item.codfilial || '';
return (
codgrupo === '01' &&
item.data_competencia === mes &&
itemFilial === filial
);
2025-12-09 20:58:11 +00:00
})
.reduce((sum, item) => sum + parseFloat(item.valor), 0);
});
});
return valores;
2025-12-10 12:45:06 +00:00
}, [data, mesesDisponiveis]);
2025-12-09 20:41:58 +00:00
2025-12-09 19:19:55 +00:00
// Função para calcular percentuais baseado no CODGRUPO 01 (FATURAMENTO LÍQUIDO)
const calcularPercentuaisPorMes = React.useCallback(
(
valoresPorMes: Record<string, number>,
codigoGrupo?: string
): Record<string, number> => {
const percentuais: Record<string, number> = {};
// Se for CODGRUPO 01, sempre retornar 100%
if (codigoGrupo === '01') {
mesesDisponiveis.forEach((mes) => {
percentuais[mes] = 100;
});
return percentuais;
}
// Usar valores memoizados do grupo 01
Object.keys(valoresPorMes).forEach((mes) => {
const valorAtual = valoresPorMes[mes];
const valorGrupo01 = valoresGrupo01PorMesMemo[mes] || 0;
if (valorGrupo01 !== 0) {
percentuais[mes] = (valorAtual / valorGrupo01) * 100;
} else {
percentuais[mes] = 0;
}
2025-12-09 19:19:55 +00:00
});
2025-12-09 19:19:55 +00:00
return percentuais;
},
[mesesDisponiveis, valoresGrupo01PorMesMemo]
);
// Função para calcular percentuais por mês e por filial baseado no CODGRUPO 01
const calcularPercentuaisPorMesPorFilial = React.useCallback(
(
valoresPorMesPorFilial: Record<string, Record<string, number>>,
codigoGrupo?: string
): Record<string, Record<string, number>> => {
const percentuaisPorMesPorFilial: Record<
string,
Record<string, number>
> = {};
// Extrair filiais únicas dos próprios valoresPorMesPorFilial para evitar dependência de arrays externos
const primeiroMes = mesesDisponiveis[0] || '';
const filiaisDisponiveis = Object.keys(
valoresPorMesPorFilial[primeiroMes] || {}
);
// Se for CODGRUPO 01, sempre retornar 100% para todas as filiais
if (codigoGrupo === '01') {
mesesDisponiveis.forEach((mes) => {
percentuaisPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
percentuaisPorMesPorFilial[mes][filial] = 100;
});
});
return percentuaisPorMesPorFilial;
}
// Usar valores memoizados do grupo 01
mesesDisponiveis.forEach((mes) => {
2025-12-09 20:41:58 +00:00
percentuaisPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
const valorAtual = valoresPorMesPorFilial[mes]?.[filial] || 0;
const valorGrupo01 =
valoresGrupo01PorMesPorFilialMemo[mes]?.[filial] || 0;
if (valorGrupo01 !== 0) {
percentuaisPorMesPorFilial[mes][filial] =
(valorAtual / valorGrupo01) * 100;
} else {
percentuaisPorMesPorFilial[mes][filial] = 0;
}
2025-12-09 20:41:58 +00:00
});
});
return percentuaisPorMesPorFilial;
},
[mesesDisponiveis, valoresGrupo01PorMesPorFilialMemo]
);
2025-12-09 20:41:58 +00:00
2025-12-09 20:58:11 +00:00
// Memoizar total do grupo 01
const totalGrupo01Memo = React.useMemo(() => {
return data
.filter((item) => {
const codgrupo = item.codgrupo || item.codigo_grupo || '';
return codgrupo === '01';
2025-12-09 19:19:55 +00:00
})
.reduce((sum, item) => sum + parseFloat(item.valor), 0);
2025-12-09 20:58:11 +00:00
}, [data]);
// Função para calcular percentual do total baseado no CODGRUPO 01
const calcularPercentualTotal = React.useCallback(
(total: number, codigoGrupo?: string): number => {
// Se for CODGRUPO 01, sempre retornar 100%
if (codigoGrupo === '01') {
return 100;
}
// Usar total memoizado do grupo 01
if (totalGrupo01Memo !== 0) {
return (total / totalGrupo01Memo) * 100;
} else {
return 0;
}
},
[totalGrupo01Memo]
);
2025-12-09 20:58:11 +00:00
const buildHierarchicalData = React.useCallback((): HierarchicalRow[] => {
const rows: HierarchicalRow[] = [];
// Hierarquia simplificada: [grupo, conta]
// Agrupar por CODGRUPO
const gruposPorCodigo = data.reduce((acc, item) => {
const codgrupo = item.codgrupo || item.codigo_grupo || '';
if (!codgrupo) return acc;
if (!acc[codgrupo]) {
acc[codgrupo] = [];
}
acc[codgrupo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
2025-12-09 19:19:55 +00:00
// Calcular valores por grupo para linhas calculadas
const valoresPorGrupo: Record<string, Record<string, number>> = {};
Object.keys(gruposPorCodigo).forEach((codgrupo) => {
valoresPorGrupo[codgrupo] = calcularValoresPorMes(
gruposPorCodigo[codgrupo]
);
2025-12-09 19:19:55 +00:00
});
// Ordenar por CODGRUPO (numericamente)
const sortedGrupos = Object.entries(gruposPorCodigo).sort(
([codA], [codB]) => {
const numA = parseInt(codA) || 0;
const numB = parseInt(codB) || 0;
if (numA !== numB) {
return numA - numB;
}
return codA.localeCompare(codB);
}
);
2025-12-10 22:35:44 +00:00
// Variáveis para armazenar valores da MARGEM BRUTA (para uso no RESULTADO LOJA)
let valoresMargemBrutaPorMes: Record<string, number> | null = null;
let valoresMargemBrutaPorMesPorFilial: Record<
string,
Record<string, number>
> | null = null;
let totalMargemBruta: number | null = null;
2025-12-09 19:19:55 +00:00
sortedGrupos.forEach(([codgrupo, items], index) => {
// Calcular total do grupo
const totalGrupo = items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const valoresGrupoPorMes = calcularValoresPorMes(items);
2025-12-09 20:41:58 +00:00
const valoresGrupoPorMesPorFilial = calcularValoresPorMesPorFilial(items);
// Linha do grupo (Level 0)
rows.push({
type: 'grupo',
level: 0,
grupo: items[0]?.grupo || codgrupo,
codigo_grupo: codgrupo,
total: totalGrupo,
isExpanded: expandedGrupos.has(codgrupo),
valoresPorMes: valoresGrupoPorMes,
2025-12-09 20:41:58 +00:00
valoresPorMesPorFilial: valoresGrupoPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresGrupoPorMes,
codgrupo
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresGrupoPorMesPorFilial,
codgrupo
),
2025-12-09 19:19:55 +00:00
percentualTotal: calcularPercentualTotal(totalGrupo, codgrupo),
});
if (expandedGrupos.has(codgrupo)) {
// Agrupar por conta dentro do grupo
const contas = items.reduce((acc, item) => {
const conta = item.conta || '';
if (!conta) return acc;
if (!acc[conta]) {
acc[conta] = [];
}
acc[conta].push(item);
return acc;
}, {} as Record<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;
}
return contaA.localeCompare(contaB);
}
);
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const valoresContaPorMes = calcularValoresPorMes(contaItems);
const valoresContaPorMesPorFilial =
calcularValoresPorMesPorFilial(contaItems);
// Linha da conta (Level 1)
rows.push({
type: 'conta',
level: 1,
grupo: items[0]?.grupo || codgrupo,
codigo_grupo: codgrupo,
conta,
codigo_conta: contaItems[0]?.codigo_conta,
total: totalConta,
isExpanded: false,
valoresPorMes: valoresContaPorMes,
2025-12-09 20:41:58 +00:00
valoresPorMesPorFilial: valoresContaPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresContaPorMes,
codgrupo
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresContaPorMesPorFilial,
codgrupo
),
2025-12-09 19:19:55 +00:00
percentualTotal: calcularPercentualTotal(totalConta, codgrupo),
});
});
}
2025-12-09 19:19:55 +00:00
2025-12-10 22:35:44 +00:00
// Adicionar linha calculada "MARGEM BRUTA" após o grupo 02
2025-12-09 19:19:55 +00:00
// Verificar se é o último grupo ou se o próximo grupo é maior que 02
const proximoCodigo = sortedGrupos[index + 1]?.[0];
const proximoNumero = proximoCodigo ? parseInt(proximoCodigo) : 999;
if (
codgrupo === '02' ||
(parseInt(codgrupo) === 2 && proximoNumero > 2)
) {
2025-12-10 22:35:44 +00:00
// Calcular MARGEM BRUTA = CODGRUPO 01 + CODGRUPO 02
const valoresGrupo01 = valoresPorGrupo['01'] || {};
const valoresGrupo02 = valoresPorGrupo['02'] || {};
2025-12-10 22:35:44 +00:00
// Calcular valores por mês para MARGEM BRUTA
2025-12-09 19:19:55 +00:00
const valoresMargemPorMes: Record<string, number> = {};
mesesDisponiveis.forEach((mes) => {
2025-12-09 19:19:55 +00:00
const valor01 = valoresGrupo01[mes] || 0;
const valor02 = valoresGrupo02[mes] || 0;
valoresMargemPorMes[mes] = valor01 + valor02;
2025-12-09 19:19:55 +00:00
});
2025-12-10 22:35:44 +00:00
// Calcular valores por mês e por filial para MARGEM BRUTA
const valoresMargemPorMesPorFilial: Record<
string,
Record<string, number>
> = {};
const valoresGrupo01PorFilial = gruposPorCodigo['01']
? calcularValoresPorMesPorFilial(gruposPorCodigo['01'])
: {};
const valoresGrupo02PorFilial = gruposPorCodigo['02']
? calcularValoresPorMesPorFilial(gruposPorCodigo['02'])
: {};
2025-12-10 12:45:06 +00:00
// Extrair filiais únicas dos valores calculados
const primeiroMes = mesesDisponiveis[0] || '';
const filiaisDisponiveis = [
...new Set([
...Object.keys(valoresGrupo01PorFilial[primeiroMes] || {}),
...Object.keys(valoresGrupo02PorFilial[primeiroMes] || {}),
]),
];
mesesDisponiveis.forEach((mes) => {
2025-12-09 20:41:58 +00:00
valoresMargemPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
2025-12-09 20:41:58 +00:00
const valor01 = valoresGrupo01PorFilial[mes]?.[filial] || 0;
const valor02 = valoresGrupo02PorFilial[mes]?.[filial] || 0;
valoresMargemPorMesPorFilial[mes][filial] = valor01 + valor02;
2025-12-09 20:41:58 +00:00
});
});
2025-12-09 19:19:55 +00:00
// Calcular total
const totalMargem = Object.values(valoresMargemPorMes).reduce(
(sum, val) => sum + val,
0
);
2025-12-09 19:19:55 +00:00
2025-12-10 22:35:44 +00:00
// Armazenar valores da MARGEM BRUTA para uso posterior no RESULTADO LOJA
valoresMargemBrutaPorMes = valoresMargemPorMes;
valoresMargemBrutaPorMesPorFilial = valoresMargemPorMesPorFilial;
totalMargemBruta = totalMargem;
2025-12-09 19:19:55 +00:00
// Adicionar linha calculada
rows.push({
type: 'calculado',
2025-12-09 19:19:55 +00:00
level: 0,
grupo: 'MARGEM BRUTA',
codigo_grupo: 'MARGEM',
2025-12-09 19:19:55 +00:00
total: totalMargem,
isExpanded: false,
valoresPorMes: valoresMargemPorMes,
2025-12-09 20:41:58 +00:00
valoresPorMesPorFilial: valoresMargemPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresMargemPorMes,
'MARGEM'
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresMargemPorMesPorFilial,
'MARGEM'
),
percentualTotal: calcularPercentualTotal(totalMargem, 'MARGEM'),
2025-12-09 19:19:55 +00:00
isCalculado: true,
});
}
2025-12-10 22:35:44 +00:00
// Adicionar linha calculada "RESULTADO LOJA" após o grupo 04 (DESPESAS)
// Verificar se é o último grupo ou se o próximo grupo é maior que 04
const proximoCodigoDespesas = sortedGrupos[index + 1]?.[0];
const proximoNumeroDespesas = proximoCodigoDespesas
? parseInt(proximoCodigoDespesas)
: 999;
if (
(codgrupo === '04' ||
(parseInt(codgrupo) === 4 && proximoNumeroDespesas > 4)) &&
valoresMargemBrutaPorMes !== null &&
valoresMargemBrutaPorMesPorFilial !== null &&
totalMargemBruta !== null
) {
// Calcular RESULTADO LOJA = MARGEM BRUTA + DESPESAS (grupo 04)
const valoresGrupo04 = valoresPorGrupo['04'] || {};
// Calcular valores por mês para RESULTADO LOJA
const valoresResultadoPorMes: Record<string, number> = {};
mesesDisponiveis.forEach((mes) => {
const valorMargem = valoresMargemBrutaPorMes![mes] || 0;
const valorDespesas = valoresGrupo04[mes] || 0;
valoresResultadoPorMes[mes] = valorMargem + valorDespesas;
});
// Calcular valores por mês e por filial para RESULTADO LOJA
const valoresResultadoPorMesPorFilial: Record<
string,
Record<string, number>
> = {};
const valoresGrupo04PorFilial = gruposPorCodigo['04']
? calcularValoresPorMesPorFilial(gruposPorCodigo['04'])
: {};
// Extrair filiais únicas dos valores calculados
const primeiroMes = mesesDisponiveis[0] || '';
const filiaisDisponiveis = [
...new Set([
...Object.keys(
valoresMargemBrutaPorMesPorFilial![primeiroMes] || {}
),
...Object.keys(valoresGrupo04PorFilial[primeiroMes] || {}),
]),
];
mesesDisponiveis.forEach((mes) => {
valoresResultadoPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
const valorMargem =
valoresMargemBrutaPorMesPorFilial![mes]?.[filial] || 0;
const valorDespesas = valoresGrupo04PorFilial[mes]?.[filial] || 0;
valoresResultadoPorMesPorFilial[mes][filial] =
valorMargem + valorDespesas;
});
});
// Calcular total
const totalResultado = Object.values(valoresResultadoPorMes).reduce(
(sum, val) => sum + val,
0
);
// Adicionar linha calculada
rows.push({
type: 'calculado',
level: 0,
grupo: 'RESULTADO LOJA',
codigo_grupo: 'RESULTADO',
total: totalResultado,
isExpanded: false,
valoresPorMes: valoresResultadoPorMes,
valoresPorMesPorFilial: valoresResultadoPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresResultadoPorMes,
'RESULTADO'
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresResultadoPorMesPorFilial,
'RESULTADO'
),
percentualTotal: calcularPercentualTotal(totalResultado, 'RESULTADO'),
isCalculado: true,
});
}
});
return rows;
}, [
data,
mesesDisponiveis,
expandedGrupos,
calcularValoresPorMes,
calcularValoresPorMesPorFilial,
calcularPercentuaisPorMes,
calcularPercentuaisPorMesPorFilial,
calcularPercentualTotal,
]);
const getRowStyle = React.useCallback(
(row: HierarchicalRow) => {
const baseStyle =
'transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30';
const linhaId = `${row.type}-${row.codigo_grupo || ''}-${
row.codigo_conta || ''
}`;
const isSelected = linhaSelecionada === linhaId;
let style = baseStyle;
if (isSelected) {
style +=
' bg-gradient-to-r from-green-100 to-emerald-100 border-l-4 border-green-500 shadow-lg';
}
switch (row.type) {
case 'grupo':
return `${style} bg-gradient-to-r from-blue-50/20 to-indigo-50/20 font-bold text-gray-900 border-b-2 border-blue-200`;
case 'calculado':
return `${style} bg-gradient-to-r from-purple-50/30 to-pink-50/30 font-bold text-purple-900 border-b-2 border-purple-300 italic`;
case 'conta':
return `${style} bg-white font-normal text-gray-600`;
default:
return style;
}
},
[linhaSelecionada]
);
const getFixedCellBackground = React.useCallback(
(row: HierarchicalRow): string => {
const linhaId = `${row.type}-${row.codigo_grupo || ''}-${
row.codigo_conta || ''
}`;
const isSelected = linhaSelecionada === linhaId;
if (isSelected) {
return 'bg-gradient-to-r from-green-100 to-emerald-100';
}
switch (row.type) {
case 'grupo':
return 'bg-gradient-to-r from-blue-50 to-indigo-50';
case 'calculado':
return 'bg-gradient-to-r from-purple-50 to-pink-50';
case 'conta':
return 'bg-white';
default:
return 'bg-white';
}
},
[linhaSelecionada]
);
2025-12-09 20:58:11 +00:00
const getIndentStyle = React.useCallback((level: number) => {
return { paddingLeft: `${level * 20}px` };
2025-12-09 20:58:11 +00:00
}, []);
const renderCellContent = React.useCallback(
(row: HierarchicalRow) => {
switch (row.type) {
case 'grupo':
return (
<div className="flex items-center gap-2 whitespace-nowrap">
<button
onClick={() => toggleGrupo(row.codigo_grupo!)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-150 ease-in-out flex items-center justify-center w-8 h-8 flex-shrink-0 transform hover:scale-105"
>
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)}
</button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<div className="flex items-center gap-2">
<span className="font-bold text-gray-900">{row.grupo}</span>
{row.codigo_grupo && (
// <span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
// {row.codigo_grupo}
// </span>
<></>
)}
</div>
</button>
2025-12-09 19:19:55 +00:00
</div>
);
case 'calculado':
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-purple-600 font-bold text-lg">=</span> */}
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
2025-12-09 19:19:55 +00:00
</div>
<div className="flex-1 text-left p-2">
<div className="flex items-center gap-2">
<span className="font-bold text-purple-700 italic">
{row.grupo}
</span>
</div>
</div>
</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;
}
},
[toggleGrupo, handleRowClick]
);
const toggleExpandAll = () => {
if (isAllExpanded) {
setExpandedGrupos(new Set());
setIsAllExpanded(false);
} else {
const todosGrupos = [
...new Set(
data.map((item) => item.codgrupo || item.codigo_grupo).filter(Boolean)
),
];
setExpandedGrupos(new Set(todosGrupos));
setIsAllExpanded(true);
}
};
const aplicarFiltros = async () => {
setIsFilterOpen(false);
setTimeout(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/dre-filial-oracle');
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const dadosCompletos = await response.json();
// Aplicar filtros nos dados
let dadosFiltrados = dadosCompletos;
// Filtro por período
if (filtros.periodoDe && filtros.periodoAte) {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
const dataItem = item.data_competencia;
return (
dataItem >= filtros.periodoDe && dataItem <= filtros.periodoAte
);
});
}
// Filtro por filial (multi-seleção)
if (filiaisSelecionadas.length > 0) {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
const filialItem = item.filial || item.codfilial || '';
return filiaisSelecionadas.includes(filialItem);
});
}
setDadosFiltrados(dadosFiltrados);
setData(dadosFiltrados);
setFiltrosAplicados(true);
// Limpar filtros do analítico ao aplicar novos filtros na tabela
setAnaliticoFiltros({
dataInicio: '',
dataFim: '',
centroCusto: '',
codigoGrupo: '',
codigoSubgrupo: '',
codigoConta: '',
codFilial: '',
linhaSelecionada: '',
excluirCentroCusto: '',
excluirCodigoConta: '',
codigosCentrosCustoSelecionados: '',
codigosContasSelecionadas: '',
});
// Extrair meses únicos dos dados filtrados
const meses = [
...new Set(
dadosFiltrados.map((item: DREItem) => item.data_competencia)
),
].sort() as string[];
setMesesDisponiveis(meses);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setLoading(false);
}
}, 100);
};
const limparFiltros = () => {
const agora = new Date();
const anoAtual = agora.getFullYear();
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
const periodoAtual = `${anoAtual}-${mesAtual}`;
setFiltros({
periodoDe: `${anoAtual}-01`,
periodoAte: periodoAtual,
filial: 'Todas',
});
setFiliaisSelecionadas([]);
setData([]);
setDadosFiltrados([]);
setFiltrosAplicados(false);
setMesesDisponiveis([]);
setIsAllExpanded(false);
setIsFilterOpen(false);
// Limpar filtros do analítico também
setAnaliticoFiltros({
dataInicio: '',
dataFim: '',
centroCusto: '',
codigoGrupo: '',
codigoSubgrupo: '',
codigoConta: '',
codFilial: '',
linhaSelecionada: '',
excluirCentroCusto: '',
excluirCodigoConta: '',
codigosCentrosCustoSelecionados: '',
codigosContasSelecionadas: '',
});
carregarPeriodosDisponiveis();
};
// Função para construir dados hierárquicos completamente expandidos (para exportação)
const buildHierarchicalDataCompleta =
React.useCallback((): HierarchicalRow[] => {
const rows: HierarchicalRow[] = [];
// Hierarquia simplificada: [grupo, conta]
// Agrupar por CODGRUPO
const gruposPorCodigo = data.reduce((acc, item) => {
const codgrupo = item.codgrupo || item.codigo_grupo || '';
if (!codgrupo) return acc;
if (!acc[codgrupo]) {
acc[codgrupo] = [];
}
acc[codgrupo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Calcular valores por grupo para linhas calculadas
const valoresPorGrupo: Record<string, Record<string, number>> = {};
Object.keys(gruposPorCodigo).forEach((codgrupo) => {
valoresPorGrupo[codgrupo] = calcularValoresPorMes(
gruposPorCodigo[codgrupo]
);
});
// Ordenar por CODGRUPO (numericamente)
const sortedGrupos = Object.entries(gruposPorCodigo).sort(
([codA], [codB]) => {
const numA = parseInt(codA) || 0;
const numB = parseInt(codB) || 0;
if (numA !== numB) {
return numA - numB;
}
return codA.localeCompare(codB);
}
);
// Variáveis para armazenar valores da MARGEM BRUTA (para uso no RESULTADO LOJA)
let valoresMargemBrutaPorMes: Record<string, number> | null = null;
let valoresMargemBrutaPorMesPorFilial: Record<
string,
Record<string, number>
> | null = null;
let totalMargemBruta: number | null = null;
sortedGrupos.forEach(([codgrupo, items], index) => {
// Calcular total do grupo
const totalGrupo = items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const valoresGrupoPorMes = calcularValoresPorMes(items);
const valoresGrupoPorMesPorFilial =
calcularValoresPorMesPorFilial(items);
// Linha do grupo (Level 0)
rows.push({
type: 'grupo',
level: 0,
grupo: items[0]?.grupo || codgrupo,
codigo_grupo: codgrupo,
total: totalGrupo,
isExpanded: true, // Sempre expandido na exportação
valoresPorMes: valoresGrupoPorMes,
valoresPorMesPorFilial: valoresGrupoPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresGrupoPorMes,
codgrupo
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresGrupoPorMesPorFilial,
codgrupo
),
percentualTotal: calcularPercentualTotal(totalGrupo, codgrupo),
});
// SEMPRE incluir todas as contas (não depende de expandedGrupos)
// Agrupar por conta dentro do grupo
const contas = items.reduce((acc, item) => {
const conta = item.conta || '';
if (!conta) return acc;
if (!acc[conta]) {
acc[conta] = [];
}
acc[conta].push(item);
return acc;
}, {} as Record<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;
}
return contaA.localeCompare(contaB);
}
);
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
const valoresContaPorMes = calcularValoresPorMes(contaItems);
const valoresContaPorMesPorFilial =
calcularValoresPorMesPorFilial(contaItems);
// Linha da conta (Level 1)
rows.push({
type: 'conta',
level: 1,
grupo: items[0]?.grupo || codgrupo,
codigo_grupo: codgrupo,
conta,
codigo_conta: contaItems[0]?.codigo_conta,
total: totalConta,
isExpanded: false,
valoresPorMes: valoresContaPorMes,
valoresPorMesPorFilial: valoresContaPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresContaPorMes,
codgrupo
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresContaPorMesPorFilial,
codgrupo
),
percentualTotal: calcularPercentualTotal(totalConta, codgrupo),
});
});
// Adicionar linha calculada "MARGEM BRUTA" após o grupo 02
// Verificar se é o último grupo ou se o próximo grupo é maior que 02
const proximoCodigo = sortedGrupos[index + 1]?.[0];
const proximoNumero = proximoCodigo ? parseInt(proximoCodigo) : 999;
if (
codgrupo === '02' ||
(parseInt(codgrupo) === 2 && proximoNumero > 2)
) {
// Calcular MARGEM BRUTA = CODGRUPO 01 + CODGRUPO 02
const valoresGrupo01 = valoresPorGrupo['01'] || {};
const valoresGrupo02 = valoresPorGrupo['02'] || {};
// Calcular valores por mês para MARGEM BRUTA
const valoresMargemPorMes: Record<string, number> = {};
mesesDisponiveis.forEach((mes) => {
const valor01 = valoresGrupo01[mes] || 0;
const valor02 = valoresGrupo02[mes] || 0;
valoresMargemPorMes[mes] = valor01 + valor02;
});
// Calcular valores por mês e por filial para MARGEM BRUTA
const valoresMargemPorMesPorFilial: Record<
string,
Record<string, number>
> = {};
const valoresGrupo01PorFilial = gruposPorCodigo['01']
? calcularValoresPorMesPorFilial(gruposPorCodigo['01'])
: {};
const valoresGrupo02PorFilial = gruposPorCodigo['02']
? calcularValoresPorMesPorFilial(gruposPorCodigo['02'])
: {};
// Extrair filiais únicas dos valores calculados
const primeiroMes = mesesDisponiveis[0] || '';
const filiaisDisponiveis = [
...new Set([
...Object.keys(valoresGrupo01PorFilial[primeiroMes] || {}),
...Object.keys(valoresGrupo02PorFilial[primeiroMes] || {}),
]),
];
mesesDisponiveis.forEach((mes) => {
valoresMargemPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
const valor01 = valoresGrupo01PorFilial[mes]?.[filial] || 0;
const valor02 = valoresGrupo02PorFilial[mes]?.[filial] || 0;
valoresMargemPorMesPorFilial[mes][filial] = valor01 + valor02;
});
});
// Calcular total
const totalMargem = Object.values(valoresMargemPorMes).reduce(
(sum, val) => sum + val,
0
);
// Armazenar valores da MARGEM BRUTA para uso posterior no RESULTADO LOJA
valoresMargemBrutaPorMes = valoresMargemPorMes;
valoresMargemBrutaPorMesPorFilial = valoresMargemPorMesPorFilial;
totalMargemBruta = totalMargem;
// Adicionar linha calculada
rows.push({
type: 'calculado',
level: 0,
grupo: 'MARGEM BRUTA',
codigo_grupo: 'MARGEM',
total: totalMargem,
isExpanded: false,
valoresPorMes: valoresMargemPorMes,
valoresPorMesPorFilial: valoresMargemPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresMargemPorMes,
'MARGEM'
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresMargemPorMesPorFilial,
'MARGEM'
),
percentualTotal: calcularPercentualTotal(totalMargem, 'MARGEM'),
isCalculado: true,
});
}
// Adicionar linha calculada "RESULTADO LOJA" após o grupo 04 (DESPESAS)
// Verificar se é o último grupo ou se o próximo grupo é maior que 04
const proximoCodigoDespesas = sortedGrupos[index + 1]?.[0];
const proximoNumeroDespesas = proximoCodigoDespesas
? parseInt(proximoCodigoDespesas)
: 999;
if (
(codgrupo === '04' ||
(parseInt(codgrupo) === 4 && proximoNumeroDespesas > 4)) &&
valoresMargemBrutaPorMes !== null &&
valoresMargemBrutaPorMesPorFilial !== null &&
totalMargemBruta !== null
) {
// Calcular RESULTADO LOJA = MARGEM BRUTA + DESPESAS (grupo 04)
const valoresGrupo04 = valoresPorGrupo['04'] || {};
// Calcular valores por mês para RESULTADO LOJA
const valoresResultadoPorMes: Record<string, number> = {};
mesesDisponiveis.forEach((mes) => {
const valorMargem = valoresMargemBrutaPorMes![mes] || 0;
const valorDespesas = valoresGrupo04[mes] || 0;
valoresResultadoPorMes[mes] = valorMargem + valorDespesas;
});
// Calcular valores por mês e por filial para RESULTADO LOJA
const valoresResultadoPorMesPorFilial: Record<
string,
Record<string, number>
> = {};
const valoresGrupo04PorFilial = gruposPorCodigo['04']
? calcularValoresPorMesPorFilial(gruposPorCodigo['04'])
: {};
// Extrair filiais únicas dos valores calculados
const primeiroMes = mesesDisponiveis[0] || '';
const filiaisDisponiveis = [
...new Set([
...Object.keys(
valoresMargemBrutaPorMesPorFilial![primeiroMes] || {}
),
...Object.keys(valoresGrupo04PorFilial[primeiroMes] || {}),
]),
];
mesesDisponiveis.forEach((mes) => {
valoresResultadoPorMesPorFilial[mes] = {};
filiaisDisponiveis.forEach((filial) => {
const valorMargem =
valoresMargemBrutaPorMesPorFilial![mes]?.[filial] || 0;
const valorDespesas = valoresGrupo04PorFilial[mes]?.[filial] || 0;
valoresResultadoPorMesPorFilial[mes][filial] =
valorMargem + valorDespesas;
});
});
// Calcular total
const totalResultado = Object.values(valoresResultadoPorMes).reduce(
(sum, val) => sum + val,
0
);
// Adicionar linha calculada
rows.push({
type: 'calculado',
level: 0,
grupo: 'RESULTADO LOJA',
codigo_grupo: 'RESULTADO',
total: totalResultado,
isExpanded: false,
valoresPorMes: valoresResultadoPorMes,
valoresPorMesPorFilial: valoresResultadoPorMesPorFilial,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresResultadoPorMes,
'RESULTADO'
),
percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(
valoresResultadoPorMesPorFilial,
'RESULTADO'
),
percentualTotal: calcularPercentualTotal(
totalResultado,
'RESULTADO'
),
isCalculado: true,
});
}
});
return rows;
}, [
data,
mesesDisponiveis,
calcularValoresPorMes,
calcularValoresPorMesPorFilial,
calcularPercentuaisPorMes,
calcularPercentuaisPorMesPorFilial,
calcularPercentualTotal,
]);
const exportarXLSX = () => {
if (!data.length) {
console.log('⚠️ Nenhum dado para exportar');
return;
}
console.log('📊 Exportando TODOS os dados expandidos para XLSX...');
const dadosCompletosExpandidos = buildHierarchicalDataCompleta();
// Determinar filiais para exportação (mesma lógica da tabela)
const filiaisParaExportacao = ordenarFiliaisNumericamente(
filtrosAplicados && filiaisSelecionadas.length > 0
? filiaisSelecionadas
: opcoesFiliais.length > 0
? opcoesFiliais
: ['']
);
const dadosExportacao = dadosCompletosExpandidos.map((row, index) => {
const linha: any = {
Linha: index + 1,
Tipo: row.type,
Nível: row.level,
Grupo: row.grupo || '',
'Código Grupo': row.codigo_grupo || '',
Conta: row.conta || '',
'Código Conta': row.codigo_conta || '',
Total: row.total || 0,
};
// Para cada mês, adicionar colunas para cada filial e depois o totalizador
mesesDisponiveis.forEach((mes) => {
// Colunas por filial
filiaisParaExportacao.forEach((filial: string) => {
// Só incluir se a filial estiver selecionada ou se não houver filtros aplicados
if (
!filtrosAplicados ||
filiaisSelecionadas.length === 0 ||
filiaisSelecionadas.includes(filial)
) {
const valorFilial =
filial &&
row.valoresPorMesPorFilial?.[mes]?.[filial] !== undefined
? row.valoresPorMesPorFilial[mes][filial]
: 0;
const percentualFilial =
filial &&
row.percentuaisPorMesPorFilial?.[mes]?.[filial] !== undefined
? row.percentuaisPorMesPorFilial[mes][filial]
: 0;
linha[`${mes} - Filial ${filial} - Valor`] = valorFilial;
linha[`${mes} - Filial ${filial} - %`] = percentualFilial;
}
});
// Totalizador do mês
const valorTotal = row.valoresPorMes?.[mes] || 0;
const percentualTotal = row.percentuaisPorMes?.[mes] || 0;
linha[`${mes} - Total - Valor`] = valorTotal;
linha[`${mes} - Total - %`] = percentualTotal;
});
return linha;
});
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(dadosExportacao);
const colWidths = [
{ wch: 8 }, // Linha
{ wch: 15 }, // Tipo
{ wch: 8 }, // Nível
{ wch: 25 }, // Grupo
{ wch: 15 }, // Código Grupo
{ wch: 35 }, // Conta
{ wch: 12 }, // Código Conta
{ wch: 15 }, // Total
];
// Larguras das colunas por mês (filiais + totalizador)
mesesDisponiveis.forEach(() => {
filiaisParaExportacao.forEach((filial: string) => {
if (
!filtrosAplicados ||
filiaisSelecionadas.length === 0 ||
filiaisSelecionadas.includes(filial)
) {
colWidths.push({ wch: 18 }); // Valor Filial
colWidths.push({ wch: 12 }); // % Filial
}
});
colWidths.push({ wch: 18 }); // Valor Total
colWidths.push({ wch: 12 }); // % Total
});
ws['!cols'] = colWidths;
XLSX.utils.book_append_sheet(wb, ws, 'DRE Filial Completo');
const resumoData = [
{
Informação: 'Período',
Valor: `${filtros.periodoDe} a ${filtros.periodoAte}`,
},
{
Informação: 'Filiais',
Valor:
filiaisSelecionadas.length > 0
? filiaisSelecionadas.join(', ')
: 'Todas',
},
{
Informação: 'Total de Registros',
Valor: dadosCompletosExpandidos.length,
},
{
Informação: 'Data de Exportação',
Valor: new Date().toLocaleString('pt-BR'),
},
];
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
wsResumo['!cols'] = [{ wch: 20 }, { wch: 30 }];
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
const dataAtual = new Date().toISOString().split('T')[0];
const nomeArquivo = `DRE_Filial_Completo_${dataAtual}.xlsx`;
XLSX.writeFile(wb, nomeArquivo);
console.log('✅ Arquivo XLSX completo exportado:', nomeArquivo);
};
2025-12-09 20:58:11 +00:00
// Memoizar dados hierárquicos para evitar recálculos desnecessários
const hierarchicalData = React.useMemo(() => {
if (!data.length || !mesesDisponiveis.length) {
return [];
}
return buildHierarchicalData();
}, [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">
Despesa Filial
</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">
<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>
<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>
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
<SheetTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 text-xs h-8 px-3 transition-all duration-150 ease-in-out hover:scale-105"
>
<Filter className="w-4 h-4" />
Filtros
</Button>
</SheetTrigger>
2025-12-09 14:41:39 +00:00
<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>
2025-12-09 14:41:39 +00:00
<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) =>
setFiltros((prev) => ({
...prev,
periodoDe: value,
}))
}
>
2025-12-09 14:41:39 +00:00
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{mesesDisponiveis.map((mes) => (
<SelectItem key={mes} value={mes}>
{mes}
</SelectItem>
2025-12-09 14:41:39 +00:00
))}
</SelectContent>
</Select>
</div>
<div>
<Label
htmlFor="periodo-ate"
className="text-xs text-gray-500"
>
ATÉ
</Label>
<Select
value={filtros.periodoAte}
onValueChange={(value) =>
setFiltros((prev) => ({
...prev,
periodoAte: value,
}))
}
>
2025-12-09 14:41:39 +00:00
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{mesesDisponiveis.map((mes) => (
<SelectItem key={mes} value={mes}>
{mes}
</SelectItem>
2025-12-09 14:41:39 +00:00
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Filial */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="filial">FILIAL</Label>
<div className="flex gap-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={selecionarTodasFiliais}
className="text-xs h-5 px-2"
>
Todas
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={limparFiliais}
className="text-xs h-5 px-2"
>
Limpar
</Button>
</div>
</div>
<Input
placeholder="Filtrar filiais..."
value={filtroFilial}
onChange={(e) => setFiltroFilial(e.target.value)}
className="h-8 text-sm"
/>
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
{opcoesFiliais
.filter((filial) => {
if (!filtroFilial) return true;
const termo = filtroFilial.toLowerCase();
return filial.toLowerCase().includes(termo);
})
.map((filial) => (
<div
key={filial}
className="flex items-center space-x-1"
>
<Checkbox
id={`filial-${filial}`}
checked={filiaisSelecionadas.includes(filial)}
onCheckedChange={() => toggleFilial(filial)}
/>
<Label
htmlFor={`filial-${filial}`}
className="text-sm font-normal cursor-pointer flex-1"
>
{filial}
</Label>
</div>
))}
</div>
{filiaisSelecionadas.length > 0 && (
<div className="text-xs text-gray-500">
{filiaisSelecionadas.length} filial(is) selecionada(s)
</div>
)}
</div>
2025-12-09 14:41:39 +00:00
</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">
<div
className="overflow-x-auto overflow-y-auto max-h-[500px]"
style={{ scrollbarWidth: 'thin' }}
>
<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) => {
const filiaisParaMes =
filtrosAplicados && filiaisSelecionadas.length > 0
? filiaisSelecionadas
: opcoesFiliais.length > 0
? opcoesFiliais
: [''];
return (
<React.Fragment key={mes}>
{/* Cabeçalhos de filiais para este mês */}
{filiaisParaMes.map((filial: string) => (
<React.Fragment key={`${mes}-${filial || 'default'}`}>
<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}
{filial && (
<>
<br />
<span className="text-[10px] font-normal text-gray-600">
Filial - {filial}
</span>
</>
)}
</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">
%
{filial && (
<>
<br />
<span className="text-[10px] font-normal text-gray-600">
Filial - {filial}
</span>
</>
)}
</th>
</React.Fragment>
))}
{/* Cabeçalhos de totalizador para este mês */}
<th className="px-2 py-2 text-right text-xs font-bold text-green-700 uppercase tracking-wide w-[120px] min-w-[120px] bg-gradient-to-r from-green-50 to-emerald-50">
{mes}
<br />
<span className="text-[10px] font-normal text-green-600">
Total
</span>
2025-12-09 20:41:58 +00:00
</th>
<th className="px-2 py-2 text-center text-xs font-bold text-green-700 uppercase tracking-wide w-[100px] min-w-[100px] bg-gradient-to-r from-green-50 to-emerald-50">
%<br />
<span className="text-[10px] font-normal text-green-600">
Total
</span>
2025-12-09 20:41:58 +00:00
</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.codigo_grupo || ''}-${
row.codigo_conta || ''
}`;
const isSelected = linhaSelecionada === linhaId;
return (
<TableRow
key={index}
row={row}
index={index}
handleRowClick={handleRowClick}
getRowStyle={getRowStyle}
getIndentStyle={getIndentStyle}
renderCellContent={renderCellContent}
mesesDisponiveis={mesesDisponiveis}
2025-12-09 20:41:58 +00:00
opcoesFiliais={opcoesFiliais}
2025-12-09 20:58:11 +00:00
filiaisSelecionadas={filiaisSelecionadas}
filtrosAplicados={filtrosAplicados}
formatCurrency={formatCurrency}
formatCurrencyWithColor={formatCurrencyWithColor}
getFixedCellBackground={getFixedCellBackground}
/>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Componente Analítico - Sempre visível, mas só carrega dados após clique */}
<div className="mt-4">
<AnaliticoComponent filtros={analiticoFiltros} />
</div>
</div>
);
}