diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index b5b4570..ffa249b 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -257,6 +257,37 @@ interface VirtualRowProps { columnSizing: Record; } +/** + * Colonnes figées à gauche : celles qui disent QUELLE ligne on lit. + * + * Sans elles, dès qu'on défile vers les totaux ou les mois, la Désignation + * sort de l'écran et plus rien n'identifie la ligne — sur 2600 px de tableau + * pour ~1150 px visibles, c'est la moitié du temps. + * + * `libelle1` a été rapprochée de `codein` dans l'ordre des colonnes : le + * figeage exige des colonnes contiguës depuis la gauche, et geler en plus + * Référence et GTIN aurait immobilisé 636 px au lieu de 406. + */ +const COLONNES_FIGEES = new Set(["select", "codein", "libelle1"]); + +/** + * Décalage `left` de chaque colonne figée, par cumul des largeurs. + * + * On s'arrête à la première colonne non figée : une colonne figée qui ne + * toucherait pas le bord flotterait au milieu du tableau. Si l'utilisateur + * masque « Code interne », le calcul se réajuste tout seul. + */ +function decalagesFiges(colonnes: { id: string; taille: number }[]): Map { + const out = new Map(); + let cumul = 0; + for (const c of colonnes) { + if (!COLONNES_FIGEES.has(c.id)) break; + out.set(c.id, cumul); + cumul += c.taille; + } + return out; +} + const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnsKey, columnSizing }: 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); @@ -274,6 +305,10 @@ const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnsKey void columnsKey; void columnSizing; // idem, pour le redimensionnement des colonnes + const cellules = row.getVisibleCells(); + const figes = decalagesFiges(cellules.map((c) => ({ id: c.column.id, taille: c.column.getSize() }))); + const dernierFige = [...figes.keys()].at(-1); + return ( {row.getVisibleCells().map((cell: Cell) => { + const gauche = figes.get(cell.column.id); + const estFige = gauche !== undefined; + const derniereFigee = estFige && cell.column.id === dernierFige; const isFlexible = cell.column.id === "libelle1" || 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" || QLIK_NETWORK_COLUMN_IDS.has(cell.column.id) || cell.column.id === "prixVente" || cell.column.id === "gamme"; const size = cell.column.getSize(); @@ -298,8 +336,15 @@ const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnsKey {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -647,32 +701,6 @@ export function HeatmapGrid({ onSelectionChange, isAdmin, nomFournisseur }: Heat /> ), }, - { - accessorKey: "reference", - header: "Référence", - size: 110, - cell: ({ getValue }) => ( - ()} - titre="Copier la référence" - className="text-[12px] font-mono opacity-80 group-hover:opacity-100" - style={{ color: "var(--text-secondary)" }} - /> - ), - }, - { - accessorKey: "gtin", - header: "EAN / GTIN", - size: 120, - cell: ({ getValue }) => ( - ()} - titre="Copier l'EAN / GTIN" - className="text-[12px] font-mono tracking-tight opacity-70 group-hover:opacity-100" - style={{ color: "var(--text-secondary)" }} - /> - ), - }, { accessorKey: "libelle1", header: "Désignation", @@ -724,6 +752,32 @@ export function HeatmapGrid({ onSelectionChange, isAdmin, nomFournisseur }: Heat ); }, }, + { + accessorKey: "reference", + header: "Référence", + size: 110, + cell: ({ getValue }) => ( + ()} + titre="Copier la référence" + className="text-[12px] font-mono opacity-80 group-hover:opacity-100" + style={{ color: "var(--text-secondary)" }} + /> + ), + }, + { + accessorKey: "gtin", + header: "EAN / GTIN", + size: 120, + cell: ({ getValue }) => ( + ()} + titre="Copier l'EAN / GTIN" + className="text-[12px] font-mono tracking-tight opacity-70 group-hover:opacity-100" + style={{ color: "var(--text-secondary)" }} + /> + ), + }, { accessorKey: "libelle3", header: "Famille", @@ -1238,21 +1292,44 @@ export function HeatmapGrid({ onSelectionChange, isAdmin, nomFournisseur }: Heat }}> {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { + {(() => { + const figesEntete = decalagesFiges( + headerGroup.headers.map((h) => ({ id: h.column.id, taille: h.getSize() })), + ); + const dernierFigeEntete = [...figesEntete.keys()].at(-1); + return headerGroup.headers.map((header) => { + const gaucheEntete = figesEntete.get(header.column.id); + const enteteFigee = gaucheEntete !== undefined; const isFlexible = header.column.id === "libelle1" || header.column.id === "libelle3"; const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || QLIK_NETWORK_COLUMN_IDS.has(header.column.id) || header.column.id === "prixVente" || header.column.id === "gamme"; const size = header.getSize(); return ( est un dégradé translucide : sans + // fond propre, les autres intitulés défileraient à + // travers l'en-tête figé. On reprend le même dégradé + // pour que la bande reste d'un seul tenant. + background: "linear-gradient(to bottom, var(--bg-elevated), var(--bg-surface))", + borderRight: header.column.id === dernierFigeEntete + ? "1px solid var(--border-strong)" + : undefined, + } : {}), }} onClick={header.column.getToggleSortingHandler()} > @@ -1278,7 +1355,8 @@ export function HeatmapGrid({ onSelectionChange, isAdmin, nomFournisseur }: Heat )} ); - })} + }); + })()} ))}