From 720cf3c7a1611630566312f27c0a802c555f1937 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 08:46:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(grille):=20filtre=20nomenclature=20en=20mu?= =?UTF-8?q?lti-s=C3=A9lection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le filtre était un ` simple : on veut pouvoir + * en retenir plusieurs, et surtout partir de « tout » pour retirer les postes + * dont on ne veut pas — ce qui est la manière naturelle de travailler quand un + * fournisseur en compte des dizaines. + * + * Convention : `null` = aucun filtre (tout s'affiche), `[]` = rien de coché + * (aucune ligne). Les distinguer évite la valeur sentinelle qu'imposerait un + * simple tableau, et rend « Tout décocher » possible sans ambiguïté. + */ export function NomenclatureFilter({ options, className }: NomenclatureFilterProps) { - const code3 = useGridStore((s) => s.filters.code3); - const setFilter = useGridStore((s) => s.setFilter); + const selection = useGridStore((s) => s.filters.code3); + const setCode3Filter = useGridStore((s) => s.setCode3Filter); + const [recherche, setRecherche] = React.useState(""); + + /** `null` = tout retenu : les cases sont alors toutes cochées. */ + const tout = selection === null; + const selectionnes = React.useMemo(() => new Set(selection ?? []), [selection]); + + const visibles = React.useMemo(() => { + const q = recherche.trim().toLowerCase(); + if (!q) return options; + return options.filter((o) => `${o.code} ${o.label}`.toLowerCase().includes(q)); + }, [options, recherche]); + + const basculer = (code: string, coche: boolean) => { + // Décocher depuis l'état « tout » doit retirer ce seul poste : on matérialise + // donc la liste complète, sans quoi le premier décochage ne ferait rien. + const base = tout ? options.map((o) => o.code) : (selection ?? []); + const suivant = coche ? [...base, code] : base.filter((c) => c !== code); + // Tout re-cocher revient à ne plus filtrer : on repasse à `null` pour que + // l'état reste lisible (et l'URL propre). + setCode3Filter(suivant.length === options.length ? null : suivant); + }; + + const libelle = tout + ? "Toute la nomenclature" + : selectionnes.size === 0 + ? "Aucune nomenclature" + : selectionnes.size === 1 + ? (options.find((o) => o.code === selection![0])?.label ?? selection![0]) + : `${selectionnes.size} / ${options.length} nomenclatures`; return ( -
-
- -
+
+ + + + - + +
+
+ + 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; }