diff --git a/src/app/(dashboard)/grid/page.tsx b/src/app/(dashboard)/grid/page.tsx index 1b06424..32be563 100644 --- a/src/app/(dashboard)/grid/page.tsx +++ b/src/app/(dashboard)/grid/page.tsx @@ -19,7 +19,8 @@ export default async function GridPage({ searchParams }: GridPageProps) { const filters = { code1: params.code1 || null, code2: params.code2 || null, - code3: params.code3 || null, + // Liste séparée par des virgules : le filtre accepte plusieurs nomenclatures. + code3: params.code3 ? String(params.code3).split(",").filter(Boolean) : null, }; // 1. Fetch available suppliers & stores diff --git a/src/app/api/grid/rows/route.ts b/src/app/api/grid/rows/route.ts index 5c0ddec..da6083b 100644 --- a/src/app/api/grid/rows/route.ts +++ b/src/app/api/grid/rows/route.ts @@ -18,10 +18,11 @@ export async function GET(request: NextRequest) { const magasin = searchParams.get("magasin") ?? "TOTAL"; const chunkSize = Math.max(50, Math.min(500, Number(searchParams.get("chunkSize") ?? 150))); + // `code3` n'est volontairement pas lu : la Grille applique ce filtre en local + // (getProductRows ne s'en sert pas) et il accepte désormais plusieurs valeurs. const filters: Partial = { code1: searchParams.get("code1"), code2: searchParams.get("code2"), - code3: searchParams.get("code3"), }; const forceRefresh = searchParams.get("refresh") === "1"; diff --git a/src/features/grid/components/grid-client.tsx b/src/features/grid/components/grid-client.tsx index 4397f56..de22072 100644 --- a/src/features/grid/components/grid-client.tsx +++ b/src/features/grid/components/grid-client.tsx @@ -58,6 +58,7 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga const rows = useGridStore((s) => s.rows); const setActiveGridQuery = useGridStore((s) => s.setActiveGridQuery); const setFilter = useGridStore((s) => s.setFilter); + const setCode3Filter = useGridStore((s) => s.setCode3Filter); const setActiveMagasin = useGridStore((s) => s.setActiveMagasin); const searchParams = useSearchParams(); const router = useRouter(); @@ -95,10 +96,10 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga if (!isMounted) return; if (prevFournisseurRef.current !== codeFournisseur) { setFilter("codeGamme", null); - setFilter("code3", null); + setCode3Filter(null); prevFournisseurRef.current = codeFournisseur; } - }, [codeFournisseur, setFilter, isMounted]); + }, [codeFournisseur, setFilter, setCode3Filter, isMounted]); const refreshToken = searchParams.get("_refresh"); useEffect(() => { @@ -108,9 +109,11 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga const params = new URLSearchParams(); params.set("fournisseur", codeFournisseur); params.set("magasin", magasin || "TOTAL"); + // `code3` n'est volontairement pas transmis : le serveur ignore ce filtre + // (getProductRows ne s'en sert pas) et la Grille l'applique en local. Le + // faire voyager relançait un chargement complet à chaque case cochée. if (filters.code1) params.set("code1", filters.code1); if (filters.code2) params.set("code2", filters.code2); - if (filters.code3) params.set("code3", filters.code3); if (refreshToken) params.set("refresh", "1"); let accumulatedRows: ProductRow[] = []; @@ -173,7 +176,7 @@ export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, maga loadRows(); return () => controller.abort(); - }, [codeFournisseur, magasin, filters.code1, filters.code2, filters.code3, refreshToken, setRows, isMounted]); + }, [codeFournisseur, magasin, filters.code1, filters.code2, refreshToken, setRows, isMounted]); // Synchroniser le magasin actif depuis la prop URL (changement de magasin sans rechargement) useEffect(() => { diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 09ca1a3..0a75f15 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -414,8 +414,11 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) { const filteredData = useMemo(() => { const { code3, codeGamme } = filters; if (!code3 && !codeGamme) return rows; + // Set plutôt que includes() : la liste peut compter des dizaines de + // nomenclatures, et le filtre est réévalué pour chaque ligne. + const nomenclatures = code3 ? new Set(code3) : null; return rows.filter(r => { - if (code3 && r.code3 !== code3) return false; + if (nomenclatures && !nomenclatures.has(r.code3)) return false; if (codeGamme) { const g = draftChanges[r.codein] ?? r.codeGamme ?? ""; const norm = g.trim() === "" ? "Aucune" : g; diff --git a/src/features/grid/components/nomenclature-filter.tsx b/src/features/grid/components/nomenclature-filter.tsx index dbc997a..d3413ff 100644 --- a/src/features/grid/components/nomenclature-filter.tsx +++ b/src/features/grid/components/nomenclature-filter.tsx @@ -2,7 +2,13 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -import { ChevronDown, Filter } from "lucide-react"; +import { ChevronDown, Filter, Search, X } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { useGridStore } from "@/features/grid/store/use-grid-store"; interface NomenclatureFilterProps { @@ -10,33 +16,143 @@ interface NomenclatureFilterProps { className?: string; } +/** + * Filtre multi-nomenclatures de la Grille. + * + * Une liste déroulante à cases plutôt qu'un ` setFilter("code3", e.target.value || null)} - className="apple-input pr-12 appearance-none w-full min-w-[240px]" - style={{ fontSize: "12px", height: "36px", paddingLeft: "36px" }} - > - - {options.map((opt) => ( - - ))} - + +
+
+ + setRecherche(e.target.value)} + onKeyDown={(e) => e.stopPropagation()} + placeholder="Filtrer les nomenclatures" + className="w-full rounded-lg pl-7 pr-2 py-1.5 text-[12px] outline-none" + style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} + /> +
+
+ + + {recherche.trim() && visibles.length > 0 && ( + + )} +
+
-
- -
+ {visibles.length === 0 ? ( +

Aucune nomenclature ne correspond.

+ ) : ( + visibles.map((opt) => ( + basculer(opt.code, !!v)} + onSelect={(e: Event) => e.preventDefault()} + > + {opt.code} + {opt.label} + + )) + )} +
+ ); } diff --git a/src/features/grid/store/use-grid-store.ts b/src/features/grid/store/use-grid-store.ts index 8bfb316..5449acc 100644 --- a/src/features/grid/store/use-grid-store.ts +++ b/src/features/grid/store/use-grid-store.ts @@ -33,7 +33,9 @@ interface GridState { clearDrafts: (codeins: string[]) => void; /** Apply saved drafts onto row.codeGamme so change indicators persist after save */ applyDraftsToRows: (draftsToApply: Record) => void; - setFilter: (key: keyof GridFilters, value: string | null) => void; + setFilter: (key: Exclude, value: string | null) => void; + /** Nomenclatures retenues. `null` = aucun filtre, `[]` = rien de coché. */ + setCode3Filter: (codes: string[] | null) => void; setDisplayDensity: (density: "compact" | "normal" | "comfortable") => void; setActiveGridQuery: (query: string) => void; restoreSnapshot: (changes: Record) => void; @@ -172,6 +174,9 @@ export const useGridStore = create()( setFilter: (key, value) => { set((state) => ({ ...state, filters: { ...state.filters, [key]: value } })); }, + setCode3Filter: (codes) => { + set((state) => ({ ...state, filters: { ...state.filters, code3: codes } })); + }, setDisplayDensity: (density) => set({ displayDensity: density }), setActiveGridQuery: (query) => set({ activeGridQuery: query }), restoreSnapshot: (changes) => { diff --git a/src/types/grid.ts b/src/types/grid.ts index 43f5cc5..fea77bd 100644 --- a/src/types/grid.ts +++ b/src/types/grid.ts @@ -149,7 +149,13 @@ export interface GridFilters { codeFournisseur: string | null; code1: string | null; code2: string | null; - code3: string | null; + /** + * Nomenclatures retenues (codes `code3`). + * `null` = aucun filtre (tout s'affiche) ; `[]` = rien de coché (aucune + * ligne). Distinguer les deux évite la valeur sentinelle qu'imposerait un + * simple tableau. + */ + code3: string[] | null; codeGamme: GammeCode | null; search: string; }