Files
CollectFlow/src/features/grid/store/use-grid-store.ts
T
Claude 720cf3c7a1 feat(grille): filtre nomenclature en multi-sélection
Le filtre était un <select> à valeur unique : impossible de retenir plusieurs
nomenclatures, ni de partir de tout pour retirer les postes non voulus — ce qui
est la manière naturelle de travailler quand un fournisseur en compte des
dizaines.

Remplacé par une liste déroulante à cases, avec recherche, « Tout cocher »,
« Tout décocher », « Ne garder que ces N » sur le résultat d'une recherche, et
une croix pour tout réafficher. Décocher depuis l'état « tout » matérialise la
liste complète puis en retire le poste, sinon le premier décochage n'aurait
rien fait.

filters.code3 passe de `string | null` à `string[] | null` : `null` = aucun
filtre, `[]` = rien de coché. Distinguer les deux évite la valeur sentinelle
qu'imposerait un simple tableau, et rend « Tout décocher » sans ambiguïté.
L'URL accepte une liste séparée par des virgules.

Au passage : la Grille rechargeait tout depuis le serveur à chaque changement
de nomenclature, alors que getProductRows ignore ce filtre et que le tri se
fait en local. Le paramètre n'est plus transmis ni dans les dépendances de
l'effet — le filtrage est désormais instantané, ce qui compte d'autant plus
avec plusieurs cases à cocher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
2026-08-07 08:46:39 +00:00

234 lines
9.8 KiB
TypeScript

"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { GammeCode, GridFilters, GridSummary, ProductRow } from "@/types/grid";
interface GridState {
/** Source data from server */
rows: ProductRow[];
/** Fast lookup by codein; not persisted */
rowsByCodein: Record<string, ProductRow>;
/** Draft edits: codein → new GammeCode */
draftChanges: Record<string, GammeCode>;
filters: GridFilters;
summary: GridSummary;
displayDensity: "compact" | "normal" | "comfortable";
/** The active URL search parameters (e.g. ?fournisseur=123&magasin=TOTAL) */
activeGridQuery: string;
/** Persisted column visibility state */
columnVisibility: Record<string, boolean>;
/** Persisted column sizing state */
columnSizing: Record<string, number>;
/** Currently displayed store: "TOTAL" | "292" | "579" */
activeMagasin: string;
setActiveMagasin: (code: string) => void;
// Actions
setRows: (rows: ProductRow[]) => void;
setDraftGamme: (codein: string, gamme: GammeCode) => void;
resetDrafts: () => void;
/** Clear specific draft changes by codein */
clearDrafts: (codeins: string[]) => void;
/** Apply saved drafts onto row.codeGamme so change indicators persist after save */
applyDraftsToRows: (draftsToApply: Record<string, GammeCode>) => void;
setFilter: (key: Exclude<keyof GridFilters, "code3">, 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<string, GammeCode>) => void;
batchSetDraftGamme: (changes: Record<string, GammeCode>) => void;
setColumnVisibility: (updater: Record<string, boolean> | ((old: Record<string, boolean>) => Record<string, boolean>)) => void;
setColumnSizing: (updater: Record<string, number> | ((old: Record<string, number>) => Record<string, number>)) => void;
}
let summaryTimer: ReturnType<typeof setTimeout> | null = null;
function indexRows(rows: ProductRow[]): Record<string, ProductRow> {
const indexed: Record<string, ProductRow> = {};
for (const row of rows) indexed[row.codein] = row;
return indexed;
}
function computeSummary(rows: ProductRow[], drafts: Record<string, GammeCode>, magasin = "TOTAL"): GridSummary {
const active = rows.filter((r) => {
const g = drafts[r.codein] ?? r.codeGamme;
return g !== "Z";
});
const getCa = (r: ProductRow) => magasin === "TOTAL" ? (r.totalCa ?? 0) : (r.caByStore?.[magasin] ?? 0);
const getQte = (r: ProductRow) => magasin === "TOTAL" ? (r.totalQuantite ?? 0) : (r.quantiteByStore?.[magasin] ?? 0);
const getMarge = (r: ProductRow) => magasin === "TOTAL" ? (r.totalMarge ?? 0) : (r.margeByStore?.[magasin] ?? 0);
const totalCa = active.reduce((s, r) => s + getCa(r), 0);
const totalMarge = active.reduce((s, r) => s + getMarge(r), 0);
return {
totalProducts: rows.length,
totalRows: active.length,
totalQuantite: active.reduce((s, r) => s + getQte(r), 0),
totalCa,
totalMarge,
tauxMargeGlobal: totalCa > 0 ? (totalMarge / totalCa) * 100 : 0,
};
}
export const useGridStore = create<GridState>()(
persist(
(set, get) => ({
rows: [],
rowsByCodein: {},
activeMagasin: "TOTAL",
draftChanges: {},
filters: {
magasin: null,
codeFournisseur: null,
code1: null,
code2: null,
code3: null,
codeGamme: null,
search: "",
},
summary: {
totalProducts: 0,
totalRows: 0,
totalQuantite: 0,
totalCa: 0,
totalMarge: 0,
tauxMargeGlobal: 0,
},
displayDensity: "normal",
activeGridQuery: "",
columnVisibility: {},
columnSizing: {},
setActiveMagasin: (code) => {
set({ activeMagasin: code });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
setRows: (rows) => {
set({ rows, rowsByCodein: indexRows(rows) });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 120);
},
setDraftGamme: (codein, gamme) => {
const { rowsByCodein, draftChanges: oldDrafts } = get();
const originalRow = rowsByCodein[codein];
const originalGamme = originalRow?.codeGamme;
const draftChanges = { ...oldDrafts };
if (gamme === originalGamme) {
// If the new value matches initial, remove it from drafts
delete draftChanges[codein];
} else {
// Otherwise, record the change
draftChanges[codein] = gamme;
}
set({ draftChanges });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
resetDrafts: () => {
set({ draftChanges: {} });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, {}, state.activeMagasin) });
}, 80);
},
clearDrafts: (codeins) => {
const draftChanges = { ...get().draftChanges };
codeins.forEach((id) => delete draftChanges[id]);
set({ draftChanges });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
/** Apply saved drafts onto row.codeGamme so isModified works after clearing drafts */
applyDraftsToRows: (draftsToApply: Record<string, GammeCode>) => {
const rows = get().rows.map(r => {
const newGamme = draftsToApply[r.codein];
if (newGamme !== undefined) {
return { ...r, codeGamme: newGamme };
}
return r;
});
set({ rows, rowsByCodein: indexRows(rows), summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
},
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) => {
set({ draftChanges: changes, summary: computeSummary(get().rows, changes, get().activeMagasin) });
},
batchSetDraftGamme: (newChanges) => {
const { rows, rowsByCodein, draftChanges: oldDrafts } = get();
const updatedDrafts = { ...oldDrafts };
Object.entries(newChanges).forEach(([codein, gamme]) => {
const originalRow = rowsByCodein[codein];
const originalGamme = originalRow?.codeGamme;
if (gamme === originalGamme) {
delete updatedDrafts[codein];
} else {
updatedDrafts[codein] = gamme;
}
});
set({ draftChanges: updatedDrafts });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
setColumnVisibility: (updater) => {
set((state) => ({
columnVisibility: typeof updater === 'function' ? updater(state.columnVisibility) : updater
}));
},
setColumnSizing: (updater) => {
set((state) => ({
columnSizing: typeof updater === 'function' ? updater(state.columnSizing) : updater
}));
},
}),
{
name: "collectflow-grid-storage",
// Only persist filters, display density, drafts, active grid query, and column visibility/sizing
partialize: (state) => ({
filters: state.filters,
displayDensity: state.displayDensity,
draftChanges: state.draftChanges,
activeGridQuery: state.activeGridQuery,
columnVisibility: state.columnVisibility,
columnSizing: state.columnSizing,
}),
}
)
);