fix: otimização do processo de expansão do drill da tabela sintética
This commit is contained in:
parent
362d422fce
commit
324730c830
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -53,6 +53,123 @@ interface HierarchicalRow {
|
|||
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() {
|
||||
const [data, setData] = useState<DREItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -277,35 +394,41 @@ export default function Teste() {
|
|||
setAnaliticoFiltros(novosFiltros);
|
||||
};
|
||||
|
||||
const toggleGroup = (grupo: string) => {
|
||||
const newExpanded = new Set(expandedGroups);
|
||||
const toggleGroup = useCallback((grupo: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(grupo)) {
|
||||
newExpanded.delete(grupo);
|
||||
} else {
|
||||
newExpanded.add(grupo);
|
||||
}
|
||||
setExpandedGroups(newExpanded);
|
||||
};
|
||||
return newExpanded;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleSubgrupo = (subgrupo: string) => {
|
||||
const newExpanded = new Set(expandedSubgrupos);
|
||||
const toggleSubgrupo = useCallback((subgrupo: string) => {
|
||||
setExpandedSubgrupos(prev => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(subgrupo)) {
|
||||
newExpanded.delete(subgrupo);
|
||||
} else {
|
||||
newExpanded.add(subgrupo);
|
||||
}
|
||||
setExpandedSubgrupos(newExpanded);
|
||||
};
|
||||
return newExpanded;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleCentro = (centro: string) => {
|
||||
const newExpanded = new Set(expandedCentros);
|
||||
const toggleCentro = useCallback((centro: string) => {
|
||||
setExpandedCentros(prev => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(centro)) {
|
||||
newExpanded.delete(centro);
|
||||
} else {
|
||||
newExpanded.add(centro);
|
||||
}
|
||||
setExpandedCentros(newExpanded);
|
||||
};
|
||||
return newExpanded;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleFiltroChange = (campo: string, valor: string) => {
|
||||
setFiltros(prev => ({
|
||||
|
|
@ -351,28 +474,29 @@ export default function Teste() {
|
|||
setContasSelecionadas([]);
|
||||
};
|
||||
|
||||
const toggleExpandAll = () => {
|
||||
const toggleExpandAll = useCallback(() => {
|
||||
if (isAllExpanded) {
|
||||
// Recolher tudo
|
||||
setExpandedGroups(new Set());
|
||||
setExpandedSubgrupos(new Set());
|
||||
setExpandedCentros(new Set());
|
||||
setIsAllExpanded(false);
|
||||
// Recolher tudo - usar startTransition para atualizações não urgentes
|
||||
startTransition(() => {
|
||||
setExpandedGroups(new Set());
|
||||
setExpandedSubgrupos(new Set());
|
||||
setExpandedCentros(new Set());
|
||||
setIsAllExpanded(false);
|
||||
});
|
||||
} else {
|
||||
// Expandir todos os grupos usando dados originais
|
||||
const todosGrupos = [...new Set(data.map(item => item.grupo))];
|
||||
setExpandedGroups(new Set(todosGrupos));
|
||||
// Expandir todos os grupos usando dados originais - usar startTransition para atualizações não urgentes
|
||||
startTransition(() => {
|
||||
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
|
||||
const todosSubgrupos = [...new Set(data.map(item => `${item.grupo}-${item.subgrupo}`))];
|
||||
setExpandedSubgrupos(new Set(todosSubgrupos));
|
||||
|
||||
// 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);
|
||||
setExpandedGroups(new Set(todosGrupos));
|
||||
setExpandedSubgrupos(new Set(todosSubgrupos));
|
||||
setExpandedCentros(new Set(todosCentros));
|
||||
setIsAllExpanded(true);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [isAllExpanded, data]);
|
||||
|
||||
const limparFiltros = () => {
|
||||
const agora = new Date();
|
||||
|
|
@ -802,12 +926,12 @@ export default function Teste() {
|
|||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => toggleGroup(row.grupo!)}
|
||||
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-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 ? (
|
||||
<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
|
||||
|
|
@ -831,12 +955,12 @@ export default function Teste() {
|
|||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<button
|
||||
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 ? (
|
||||
<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
|
||||
|
|
@ -856,12 +980,12 @@ export default function Teste() {
|
|||
onClick={() =>
|
||||
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 ? (
|
||||
<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
|
||||
|
|
@ -899,7 +1023,7 @@ export default function Teste() {
|
|||
|
||||
const hierarchicalData = buildHierarchicalData();
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className="w-full max-w-none mx-auto p-2">
|
||||
{/* Header Section */}
|
||||
<div className="mb-2">
|
||||
|
|
@ -911,17 +1035,17 @@ export default function Teste() {
|
|||
Demonstração do Resultado do Exercício
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controles */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Botão de Expandir/Recolher */}
|
||||
<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"
|
||||
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 ? (
|
||||
<>
|
||||
|
|
@ -934,7 +1058,7 @@ export default function Teste() {
|
|||
Expandir Tudo
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Button>
|
||||
|
||||
{/* Botão de Filtro */}
|
||||
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
||||
|
|
@ -969,7 +1093,7 @@ export default function Teste() {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="periodo-ate" className="text-xs text-gray-500">ATÉ</Label>
|
||||
<Select value={filtros.periodoAte} onValueChange={(value) => handleFiltroChange('periodoAte', value)}>
|
||||
|
|
@ -982,9 +1106,9 @@ export default function Teste() {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grupo
|
||||
<div className="grid gap-2">
|
||||
|
|
@ -1023,7 +1147,7 @@ export default function Teste() {
|
|||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="centro-custo">CENTRO DE CUSTO</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -1042,7 +1166,7 @@ export default function Teste() {
|
|||
Limpar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
|
||||
{opcoesCentrosCusto.map(centro => (
|
||||
<div key={centro} className="flex items-center space-x-1">
|
||||
|
|
@ -1065,7 +1189,7 @@ export default function Teste() {
|
|||
{centrosCustoSelecionados.length} centro(s) selecionado(s)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conta */}
|
||||
<div className="grid gap-2">
|
||||
|
|
@ -1090,7 +1214,7 @@ export default function Teste() {
|
|||
>
|
||||
Limpar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
|
||||
{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-[100px] text-center text-xs text-gray-500">
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
<div className="flex-1 min-w-[120px] text-right">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Body */}
|
||||
<div className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
|
||||
{hierarchicalData.map((row, index) => (
|
||||
<div
|
||||
<TableRow
|
||||
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
|
||||
)}`}
|
||||
>
|
||||
<div
|
||||
className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden"
|
||||
style={getIndentStyle(row.level)}
|
||||
>
|
||||
{renderCellContent(row)}
|
||||
</div>
|
||||
{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>
|
||||
))}
|
||||
<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>
|
||||
row={row}
|
||||
index={index}
|
||||
toggleGroup={toggleGroup}
|
||||
toggleSubgrupo={toggleSubgrupo}
|
||||
toggleCentro={toggleCentro}
|
||||
handleRowClick={handleRowClick}
|
||||
getRowStyle={getRowStyle}
|
||||
getIndentStyle={getIndentStyle}
|
||||
renderCellContent={renderCellContent}
|
||||
mesesDisponiveis={mesesDisponiveis}
|
||||
formatCurrency={formatCurrency}
|
||||
formatCurrencyWithColor={formatCurrencyWithColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue