fix: otimização do processo de expansão do drill da tabela sintética

This commit is contained in:
Alessandro Gonçaalves 2025-10-22 10:23:54 -03:00
parent 362d422fce
commit 324730c830
1 changed files with 195 additions and 135 deletions

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2 } from "lucide-react"; import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2 } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback, startTransition, memo } from "react";
import AnaliticoComponent from "./analitico"; import AnaliticoComponent from "./analitico";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -53,6 +53,123 @@ interface HierarchicalRow {
isCalculado?: boolean; isCalculado?: boolean;
} }
// Componente memoizado para linhas da tabela
const TableRow = memo(({
row,
index,
toggleGroup,
toggleSubgrupo,
toggleCentro,
handleRowClick,
getRowStyle,
getIndentStyle,
renderCellContent,
mesesDisponiveis,
formatCurrency,
formatCurrencyWithColor
}: {
row: HierarchicalRow;
index: number;
toggleGroup: (grupo: string) => void;
toggleSubgrupo: (subgrupo: string) => void;
toggleCentro: (centro: string) => void;
handleRowClick: (row: HierarchicalRow, mes?: string) => void;
getRowStyle: (row: HierarchicalRow) => string;
getIndentStyle: (level: number) => React.CSSProperties;
renderCellContent: (row: HierarchicalRow) => React.ReactNode;
mesesDisponiveis: string[];
formatCurrency: (value: number) => string;
formatCurrencyWithColor: (value: number) => { formatted: string; isNegative: boolean };
}) => {
return (
<div
key={index}
className={`flex items-center gap-2 px-4 py-1 text-sm border-b border-gray-100 hover:bg-gray-50 transition-all duration-200 ease-in-out ${getRowStyle(
row
)}`}
>
<div
className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden"
style={getIndentStyle(row.level)}
>
{renderCellContent(row)}
</div>
{/* Colunas de valores por mês */}
{mesesDisponiveis.map((mes) => (
<div key={mes} className="flex min-w-[240px] max-w-[300px] gap-2">
<div
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
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>
);
})()
: "-"}
</div>
<div
className="flex-1 min-w-[100px] text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
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)}%`
: "-"}
</div>
</div>
))}
{/* Coluna Total */}
<div
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
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>
);
})()}
</div>
</div>
);
});
TableRow.displayName = 'TableRow';
export default function Teste() { export default function Teste() {
const [data, setData] = useState<DREItem[]>([]); const [data, setData] = useState<DREItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -277,35 +394,41 @@ export default function Teste() {
setAnaliticoFiltros(novosFiltros); setAnaliticoFiltros(novosFiltros);
}; };
const toggleGroup = (grupo: string) => { const toggleGroup = useCallback((grupo: string) => {
const newExpanded = new Set(expandedGroups); setExpandedGroups(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(grupo)) { if (newExpanded.has(grupo)) {
newExpanded.delete(grupo); newExpanded.delete(grupo);
} else { } else {
newExpanded.add(grupo); newExpanded.add(grupo);
} }
setExpandedGroups(newExpanded); return newExpanded;
}; });
}, []);
const toggleSubgrupo = (subgrupo: string) => { const toggleSubgrupo = useCallback((subgrupo: string) => {
const newExpanded = new Set(expandedSubgrupos); setExpandedSubgrupos(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(subgrupo)) { if (newExpanded.has(subgrupo)) {
newExpanded.delete(subgrupo); newExpanded.delete(subgrupo);
} else { } else {
newExpanded.add(subgrupo); newExpanded.add(subgrupo);
} }
setExpandedSubgrupos(newExpanded); return newExpanded;
}; });
}, []);
const toggleCentro = (centro: string) => { const toggleCentro = useCallback((centro: string) => {
const newExpanded = new Set(expandedCentros); setExpandedCentros(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(centro)) { if (newExpanded.has(centro)) {
newExpanded.delete(centro); newExpanded.delete(centro);
} else { } else {
newExpanded.add(centro); newExpanded.add(centro);
} }
setExpandedCentros(newExpanded); return newExpanded;
}; });
}, []);
const handleFiltroChange = (campo: string, valor: string) => { const handleFiltroChange = (campo: string, valor: string) => {
setFiltros(prev => ({ setFiltros(prev => ({
@ -351,28 +474,29 @@ export default function Teste() {
setContasSelecionadas([]); setContasSelecionadas([]);
}; };
const toggleExpandAll = () => { const toggleExpandAll = useCallback(() => {
if (isAllExpanded) { if (isAllExpanded) {
// Recolher tudo // Recolher tudo - usar startTransition para atualizações não urgentes
setExpandedGroups(new Set()); startTransition(() => {
setExpandedSubgrupos(new Set()); setExpandedGroups(new Set());
setExpandedCentros(new Set()); setExpandedSubgrupos(new Set());
setIsAllExpanded(false); setExpandedCentros(new Set());
setIsAllExpanded(false);
});
} else { } else {
// Expandir todos os grupos usando dados originais // Expandir todos os grupos usando dados originais - usar startTransition para atualizações não urgentes
const todosGrupos = [...new Set(data.map(item => item.grupo))]; startTransition(() => {
setExpandedGroups(new Set(todosGrupos)); const todosGrupos = [...new Set(data.map(item => item.grupo))];
const todosSubgrupos = [...new Set(data.map(item => `${item.grupo}-${item.subgrupo}`))];
const todosCentros = [...new Set(data.map(item => `${item.grupo}-${item.subgrupo}-${item.centro_custo}`))];
// Expandir todos os subgrupos usando dados originais setExpandedGroups(new Set(todosGrupos));
const todosSubgrupos = [...new Set(data.map(item => `${item.grupo}-${item.subgrupo}`))]; setExpandedSubgrupos(new Set(todosSubgrupos));
setExpandedSubgrupos(new Set(todosSubgrupos)); setExpandedCentros(new Set(todosCentros));
setIsAllExpanded(true);
// Expandir todos os centros de custo usando dados originais (isso também expande as contas automaticamente) });
const todosCentros = [...new Set(data.map(item => `${item.grupo}-${item.subgrupo}-${item.centro_custo}`))];
setExpandedCentros(new Set(todosCentros));
setIsAllExpanded(true);
} }
}; }, [isAllExpanded, data]);
const limparFiltros = () => { const limparFiltros = () => {
const agora = new Date(); const agora = new Date();
@ -802,12 +926,12 @@ export default function Teste() {
<div className="flex items-center gap-2 whitespace-nowrap"> <div className="flex items-center gap-2 whitespace-nowrap">
<button <button
onClick={() => toggleGroup(row.grupo!)} onClick={() => toggleGroup(row.grupo!)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0" 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 ? ( {row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600" /> <ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : ( ) : (
<ChevronRight className="w-4 h-4 text-blue-600" /> <ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)} )}
</button> </button>
<button <button
@ -831,12 +955,12 @@ export default function Teste() {
<div className="flex items-center gap-2 whitespace-nowrap"> <div className="flex items-center gap-2 whitespace-nowrap">
<button <button
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)} onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0" 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 ? ( {row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600" /> <ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : ( ) : (
<ChevronRight className="w-4 h-4 text-blue-600" /> <ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)} )}
</button> </button>
<button <button
@ -856,12 +980,12 @@ export default function Teste() {
onClick={() => onClick={() =>
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`) toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
} }
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0" 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 ? ( {row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600" /> <ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : ( ) : (
<ChevronRight className="w-4 h-4 text-blue-600" /> <ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)} )}
</button> </button>
<button <button
@ -899,7 +1023,7 @@ export default function Teste() {
const hierarchicalData = buildHierarchicalData(); const hierarchicalData = buildHierarchicalData();
return ( return (
<div className="w-full max-w-none mx-auto p-2"> <div className="w-full max-w-none mx-auto p-2">
{/* Header Section */} {/* Header Section */}
<div className="mb-2"> <div className="mb-2">
@ -911,17 +1035,17 @@ export default function Teste() {
Demonstração do Resultado do Exercício Demonstração do Resultado do Exercício
</p> </p>
</div> </div>
</div> </div>
{/* Controles */} {/* Controles */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Botão de Expandir/Recolher */} {/* Botão de Expandir/Recolher */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={toggleExpandAll} onClick={toggleExpandAll}
disabled={!filtrosAplicados || hierarchicalData.length === 0} disabled={!filtrosAplicados || hierarchicalData.length === 0}
className="flex items-center gap-2 text-xs h-8 px-3" 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 ? ( {isAllExpanded ? (
<> <>
@ -934,7 +1058,7 @@ export default function Teste() {
Expandir Tudo Expandir Tudo
</> </>
)} )}
</Button> </Button>
{/* Botão de Filtro */} {/* Botão de Filtro */}
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}> <Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
@ -969,7 +1093,7 @@ export default function Teste() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> <div>
<Label htmlFor="periodo-ate" className="text-xs text-gray-500">ATÉ</Label> <Label htmlFor="periodo-ate" className="text-xs text-gray-500">ATÉ</Label>
<Select value={filtros.periodoAte} onValueChange={(value) => handleFiltroChange('periodoAte', value)}> <Select value={filtros.periodoAte} onValueChange={(value) => handleFiltroChange('periodoAte', value)}>
@ -982,9 +1106,9 @@ export default function Teste() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div>
</div>
</div> </div>
</div>
</div>
{/* Grupo {/* Grupo
<div className="grid gap-2"> <div className="grid gap-2">
@ -1023,7 +1147,7 @@ export default function Teste() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="centro-custo">CENTRO DE CUSTO</Label> <Label htmlFor="centro-custo">CENTRO DE CUSTO</Label>
<div className="flex gap-1"> <div className="flex gap-1">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
@ -1042,7 +1166,7 @@ export default function Teste() {
Limpar Limpar
</Button> </Button>
</div> </div>
</div> </div>
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1"> <div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
{opcoesCentrosCusto.map(centro => ( {opcoesCentrosCusto.map(centro => (
<div key={centro} className="flex items-center space-x-1"> <div key={centro} className="flex items-center space-x-1">
@ -1065,7 +1189,7 @@ export default function Teste() {
{centrosCustoSelecionados.length} centro(s) selecionado(s) {centrosCustoSelecionados.length} centro(s) selecionado(s)
</div> </div>
)} )}
</div> </div>
{/* Conta */} {/* Conta */}
<div className="grid gap-2"> <div className="grid gap-2">
@ -1090,7 +1214,7 @@ export default function Teste() {
> >
Limpar Limpar
</Button> </Button>
</div> </div>
</div> </div>
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1"> <div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
{opcoesContas.map(conta => ( {opcoesContas.map(conta => (
@ -1252,96 +1376,32 @@ export default function Teste() {
<div className="flex-1 min-w-[120px] text-right">{mes}</div> <div className="flex-1 min-w-[120px] text-right">{mes}</div>
<div className="flex-1 min-w-[100px] text-center text-xs text-gray-500"> <div className="flex-1 min-w-[100px] text-center text-xs text-gray-500">
% %
</div>
</div> </div>
</div> ))}
))}
<div className="flex-1 min-w-[120px] text-right">Total</div> <div className="flex-1 min-w-[120px] text-right">Total</div>
</div>
</div> </div>
</div>
</div> </div>
{/* Table Body */} {/* Table Body */}
<div className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"> <div className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
{hierarchicalData.map((row, index) => ( {hierarchicalData.map((row, index) => (
<div <TableRow
key={index} key={index}
className={`flex items-center gap-2 px-4 py-1 text-sm border-b border-gray-100 hover:bg-gray-50 transition-colors ${getRowStyle( row={row}
row index={index}
)}`} toggleGroup={toggleGroup}
> toggleSubgrupo={toggleSubgrupo}
<div toggleCentro={toggleCentro}
className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden" handleRowClick={handleRowClick}
style={getIndentStyle(row.level)} getRowStyle={getRowStyle}
> getIndentStyle={getIndentStyle}
{renderCellContent(row)} renderCellContent={renderCellContent}
</div> mesesDisponiveis={mesesDisponiveis}
{mesesDisponiveis.map((mes) => ( formatCurrency={formatCurrency}
<div key={mes} className="flex min-w-[240px] max-w-[300px] gap-2"> formatCurrencyWithColor={formatCurrencyWithColor}
<div />
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
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>
);
})()
: "-"}
</div>
<div
className="flex-1 min-w-[100px] text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
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)}%`
: "-"}
</div>
</div>
))}
<div
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
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>
);
})()}
</div>
</div>
))} ))}
</div> </div>
</div> </div>