From 9aca3b8eadd9ada1369ded07f0dc04312fb80e8f Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 18 Mar 2026 20:34:23 +0100 Subject: [PATCH] feat: Add virtualized heatmap grid component with dynamic columns, filtering, and row selection. --- src/app/(dashboard)/grid/page.tsx | 8 +- src/features/grid/components/grid-client.tsx | 5 +- .../grid/components/grid-filter-bar.tsx | 16 +++- src/features/grid/components/heatmap-grid.tsx | 37 ++++++++-- .../grid/components/nomenclature-filter.tsx | 73 +++---------------- 5 files changed, 57 insertions(+), 82 deletions(-) diff --git a/src/app/(dashboard)/grid/page.tsx b/src/app/(dashboard)/grid/page.tsx index 3a064b9..2185bec 100644 --- a/src/app/(dashboard)/grid/page.tsx +++ b/src/app/(dashboard)/grid/page.tsx @@ -1,4 +1,4 @@ -import { getAvailableNomenclature, getFournisseurs, getGridData, getMagasins } from "@/features/grid/actions"; +import { getFournisseurs, getGridData, getMagasins } from "@/features/grid/actions"; import { GridClient } from "@/features/grid/components/grid-client"; import { SupplierSelectionLanding } from "@/features/grid/components/supplier-selection-landing"; @@ -22,11 +22,10 @@ export default async function GridPage({ searchParams }: GridPageProps) { code3: params.code3 || null, }; - // 1. Fetch available suppliers, stores & nomenclature hierarchy - const [fournisseurs, magasins, nomenclature] = await Promise.all([ + // 1. Fetch available suppliers & stores + const [fournisseurs, magasins] = await Promise.all([ getFournisseurs(), getMagasins(), - getAvailableNomenclature() ]); // 2. If no supplier selected, show compact selection UI @@ -48,7 +47,6 @@ export default async function GridPage({ searchParams }: GridPageProps) { fournisseurs={fournisseurs} magasins={magasins} magasin={magasin} - nomenclature={nomenclature} /> ); } diff --git a/src/features/grid/components/grid-client.tsx b/src/features/grid/components/grid-client.tsx index fa964a2..c9a1aae 100644 --- a/src/features/grid/components/grid-client.tsx +++ b/src/features/grid/components/grid-client.tsx @@ -22,10 +22,9 @@ interface GridClientProps { fournisseurs: { code: string; nom: string }[]; magasins: { code: string; nom: string }[]; magasin: string; - nomenclature: any; // Type defined in components if needed } -export function GridClient({ initialRows, nomFournisseur, fournisseurs, magasins, magasin, nomenclature }: GridClientProps) { +export function GridClient({ initialRows, nomFournisseur, fournisseurs, magasins, magasin }: GridClientProps) { const setRows = useGridStore((s) => s.setRows); const setActiveGridQuery = useGridStore((s) => s.setActiveGridQuery); const searchParams = useSearchParams(); @@ -121,7 +120,7 @@ export function GridClient({ initialRows, nomFournisseur, fournisseurs, magasins {/* Filters */}
- +
{/* Bulk toolbar (contextual) */} diff --git a/src/features/grid/components/grid-filter-bar.tsx b/src/features/grid/components/grid-filter-bar.tsx index d6c55e6..8ed494c 100644 --- a/src/features/grid/components/grid-filter-bar.tsx +++ b/src/features/grid/components/grid-filter-bar.tsx @@ -22,15 +22,25 @@ import { NomenclatureFilter } from "./nomenclature-filter"; interface GridFilterBarProps { fournisseurs: { code: string; nom: string }[]; magasins: { code: string; nom: string }[]; - nomenclature: any; } -export function GridFilterBar({ fournisseurs, magasins, nomenclature }: GridFilterBarProps) { +export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) { const { filters, setFilter, rows, draftChanges } = useGridStore(); const router = useRouter(); const searchParams = useSearchParams(); const [isRefreshing, setIsRefreshing] = React.useState(false); + // Options nomenclature (famille) construites depuis les rows chargées + const code3Options = useMemo(() => { + const map = new Map(); + rows.forEach(r => { + if (r.code3 && r.libelle3) map.set(r.code3, r.libelle3); + }); + return Array.from(map.entries()) + .map(([code, label]) => ({ code, label })) + .sort((a, b) => a.code.localeCompare(b.code, undefined, { numeric: true })); + }, [rows]); + // Compute dynamic counts and present Gammes const { gammeCounts, activeGammes } = React.useMemo(() => { const counts: Record = { Total: 0 }; @@ -195,7 +205,7 @@ export function GridFilterBar({ fournisseurs, magasins, nomenclature }: GridFilt {/* Nomenclature filter */} - + ); } diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 2a43bb0..79700a0 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -178,7 +178,23 @@ GridRow.displayName = "GridRow"; export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { // L'abonnement doit être minimal ici ! PAS de draftChanges ni de setDraftGamme. - const { rows, filters, displayDensity } = useGridStore(); + const { rows, filters, displayDensity, draftChanges } = useGridStore(); + + // Filtre client-side par code3 (famille) et codeGamme + const filteredData = useMemo(() => { + const { code3, codeGamme } = filters; + if (!code3 && !codeGamme) return rows; + return rows.filter(r => { + if (code3 && r.code3 !== code3) return false; + if (codeGamme) { + const g = draftChanges[r.codein] ?? r.codeGamme ?? ""; + const norm = g.trim() === "" ? "Aucune" : g; + if (norm !== codeGamme) return false; + } + return true; + }); + }, [rows, filters, draftChanges]); + const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState({}); const { columnVisibility, setColumnVisibility, columnSizing, setColumnSizing } = useGridStore(); @@ -351,12 +367,17 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { { accessorKey: "libelle3", header: "Famille", - size: 160, - cell: ({ getValue }) => ( - - {getValue()} - - ), + size: 200, + cell: ({ row }) => { + const code = row.original.code3; + const label = row.original.libelle3; + const display = code && label ? `${code} — ${label}` : (label || code || ""); + return ( + + {display} + + ); + }, }, { accessorKey: "score", @@ -452,7 +473,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) { ], [MONTHS_12]); // Dépendances extrêmement stables : pas de re-render du header ! const table = useReactTable({ - data: rows, + data: filteredData, columns, state: { sorting, globalFilter: filters.search, rowSelection, columnVisibility, columnSizing }, onSortingChange: setSorting, diff --git a/src/features/grid/components/nomenclature-filter.tsx b/src/features/grid/components/nomenclature-filter.tsx index 459648e..dbc997a 100644 --- a/src/features/grid/components/nomenclature-filter.tsx +++ b/src/features/grid/components/nomenclature-filter.tsx @@ -1,67 +1,18 @@ "use client"; import * as React from "react"; -import { useRouter, useSearchParams } from "next/navigation"; import { cn } from "@/lib/utils"; import { ChevronDown, Filter } from "lucide-react"; +import { useGridStore } from "@/features/grid/store/use-grid-store"; interface NomenclatureFilterProps { - hierarchy: any; + options: { code: string; label: string }[]; className?: string; } -export function NomenclatureFilter({ hierarchy, className }: NomenclatureFilterProps) { - const router = useRouter(); - const searchParams = useSearchParams(); - - const selected1 = searchParams.get("code1"); - const selected2 = searchParams.get("code2"); - const selected3 = searchParams.get("code3"); - - // Valeur composite pour le select : "type:code" - const currentValue = selected3 ? `code3:${selected3}` : - selected2 ? `code2:${selected2}` : - selected1 ? `code1:${selected1}` : ""; - - const updateFilter = (value: string) => { - const params = new URLSearchParams(searchParams.toString()); - - // Reset all nomenclature filters - params.delete("code1"); - params.delete("code2"); - params.delete("code3"); - - if (value) { - const [type, code] = value.split(":"); - params.set(type, code); - } - - router.push(`/grid?${params.toString()}`); - }; - - // Aplatir la hiérarchie pour le menu unique et trier par code - const options: { label: string; value: string; level: number; code: string }[] = []; - - // Sort Secteurs - const sortedSecteurs = Object.entries(hierarchy).sort((a, b) => a[0].localeCompare(b[0], undefined, { numeric: true })); - - sortedSecteurs.forEach(([c1, s]: [string, any]) => { - options.push({ label: s.label, value: `code1:${c1}`, level: 0, code: c1 }); - - // Sort Rayons - const sortedRayons = Object.entries(s.children).sort((a, b) => a[0].localeCompare(b[0], undefined, { numeric: true })); - - sortedRayons.forEach(([c2, r]: [string, any]) => { - options.push({ label: r.label, value: `code2:${c2}`, level: 1, code: c2 }); - - // Sort Familles - const sortedFamilles = [...r.children].sort((a, b) => a.code.localeCompare(b.code, undefined, { numeric: true })); - - sortedFamilles.forEach((f: any) => { - options.push({ label: f.label, value: `code3:${f.code}`, level: 2, code: f.code }); - }); - }); - }); +export function NomenclatureFilter({ options, className }: NomenclatureFilterProps) { + const code3 = useGridStore((s) => s.filters.code3); + const setFilter = useGridStore((s) => s.setFilter); return (
@@ -70,19 +21,15 @@ export function NomenclatureFilter({ hierarchy, className }: NomenclatureFilterP