diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 065bd09..e4b05ba 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -74,8 +74,103 @@ interface HeatmapGridProps { onSelectionChange?: (codeins: string[]) => void; } +// ========================================================================= +// OPTIMISATION PERFORMANCES (React.memo + Zustand Selectors granulaires) +// ========================================================================= + +// 1. Composant isolé pour la Cellule Gamme (évite le re-render des colonnes) +const GammeCell = React.memo(({ row }: { row: ProductRow }) => { + // Abonnement ultra-ciblé : la cellule ne re-render que si SA valeur change + const codein = row.codein; + const isDraft = useGridStore((s) => s.draftChanges[codein] !== undefined); + const effectiveGamme = useGridStore((s) => s.draftChanges[codein] ?? row.codeGamme); + const setDraftGamme = useGridStore((s) => s.setDraftGamme); + + const isModified = row.codeGamme !== row.codeGammeInit && row.codeGammeInit !== null; + + return ( +
+ setDraftGamme(codein, g)} + /> + {isModified && !isDraft && ( +
+ +
+ )} +
+ ); +}); +GammeCell.displayName = "GammeCell"; + +import { Row } from "@tanstack/react-table"; +import { VirtualItem } from "@tanstack/react-virtual"; + +// 2. Composant isolé pour la Ligne Virtuelle +// (Gère l'état grisé "Z" localement sans faire re-render toute la grid) +interface VirtualRowProps { + virtualRow: VirtualItem; + row: Row; + rowHeight: number; + isSelected: boolean; +} + +const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected }: VirtualRowProps) => { + // Uniquement la ligne concernée écoute son propre changement pour l'effet visuel + const effectiveGamme = useGridStore((s) => s.draftChanges[row.original.codein] ?? row.original.codeGamme); + + return ( + row.toggleSelected()} + className={cn( + "absolute w-full flex items-center cursor-pointer transition-all duration-200 group/row", + effectiveGamme === "Z" && "opacity-40 grayscale-[0.5] hover:grayscale-0 hover:opacity-100" + )} + style={{ + height: `${rowHeight}px`, + transform: `translateY(${virtualRow.start}px)`, + borderBottom: "1px solid var(--border)", + background: isSelected ? "var(--accent-bg)" : "transparent", + borderLeft: isSelected ? "3px solid var(--accent)" : "3px solid transparent", + }} + > + {row.getVisibleCells().map((cell: any) => { + const isFlexible = cell.column.id === "libelle1" || cell.column.id === "ai" || cell.column.id === "libelle3"; + const isCenter = cell.column.id === "totalQuantite" || cell.column.id === "totalCa" || cell.column.id === "totalMarge" || cell.column.id.startsWith("month_") || cell.column.id === "gammeInitial" || cell.column.id === "score" || cell.column.id === "gamme"; + const size = cell.column.getSize(); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + ); +}); +GridRow.displayName = "GridRow"; + +// ========================================================================= + export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { - const { rows, draftChanges, setDraftGamme, filters, displayDensity } = useGridStore(); + // L'abonnement doit être minimal ici ! PAS de draftChanges ni de setDraftGamme. + const { rows, filters, displayDensity } = useGridStore(); const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState({}); const [isMounted, setIsMounted] = useState(false); @@ -303,25 +398,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { id: "gamme", header: () =>
Gamme
, size: 110, - cell: ({ row }) => { - const effectiveGamme = (draftChanges[row.original.codein] ?? row.original.codeGamme) as GammeCode | null; - const isModified = row.original.codeGamme !== row.original.codeGammeInit && row.original.codeGammeInit !== null; - - return ( -
- setDraftGamme(row.original.codein, g)} - /> - {isModified && !draftChanges[row.original.codein] && ( -
- -
- )} -
- ); - }, + cell: ({ row }) => , }, { id: "ai", @@ -330,7 +407,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { enableSorting: false, cell: ({ row }) => , }, - ], [draftChanges, setDraftGamme, MONTHS_12]); + ], [MONTHS_12]); // Dépendances extrêmement stables : pas de re-render du header ! const table = useReactTable({ data: rows, @@ -428,51 +505,17 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { {rowVirtualizer.getVirtualItems().map((virtualRow) => { const row = tableRows[virtualRow.index]; - const effectiveGamme = draftChanges[row.original.codein] ?? row.original.codeGamme; const isSelected = row.getIsSelected(); + return ( - row.toggleSelected()} - className={cn( - "absolute w-full flex items-center cursor-pointer transition-all duration-200 group/row", - effectiveGamme === "Z" && "opacity-40 grayscale-[0.5] hover:grayscale-0 hover:opacity-100" - )} - style={{ - height: `${rowHeight}px`, - transform: `translateY(${virtualRow.start}px)`, - borderBottom: "1px solid var(--border)", - background: isSelected ? "var(--accent-bg)" : "transparent", - borderLeft: isSelected ? "3px solid var(--accent)" : "3px solid transparent", - }} - > - {row.getVisibleCells().map((cell) => { - const isFlexible = cell.column.id === "libelle1" || cell.column.id === "ai" || cell.column.id === "libelle3"; - const isCenter = cell.column.id === "totalQuantite" || cell.column.id === "totalCa" || cell.column.id === "totalMarge" || cell.column.id.startsWith("month_") || cell.column.id === "gammeInitial" || cell.column.id === "score" || cell.column.id === "gamme"; - const size = cell.column.getSize(); - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - +
+ +
); })}