"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[]; /** Index O(1) pour les lookups par codein (Map) */ rowsIndex: Map; /** Draft edits: codein → new GammeCode */ draftChanges: Record; 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; /** Persisted column sizing state */ columnSizing: Record; /** Currently displayed store: "TOTAL" | "292" | "579" */ activeMagasin: string; setActiveMagasin: (code: string) => void; // Actions setRows: (rows: ProductRow[]) => void; appendRows: (newRows: 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) => void; setFilter: (key: keyof GridFilters, value: string | null) => void; setDisplayDensity: (density: "compact" | "normal" | "comfortable") => void; setActiveGridQuery: (query: string) => void; restoreSnapshot: (changes: Record) => void; batchSetDraftGamme: (changes: Record) => void; setColumnVisibility: (updater: Record | ((old: Record) => Record)) => void; setColumnSizing: (updater: Record | ((old: Record) => Record)) => void; } function computeSummary(rows: ProductRow[], drafts: Record, 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()( persist( (set, get) => ({ rows: [], rowsIndex: new Map(), 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) => { const { rows, draftChanges } = get(); set({ activeMagasin: code, summary: computeSummary(rows, draftChanges, code) }); }, setRows: (rows) => { // Construire l'index en O(n) une seule fois au lieu de O(n) à chaque lookup. const rowsIndex = new Map(rows.map(r => [r.codein, r])); set({ rows, rowsIndex, summary: computeSummary(rows, get().draftChanges, get().activeMagasin) }); }, appendRows: (newRows) => { const { rows, rowsIndex, draftChanges, activeMagasin } = get(); // O(n) uniquement sur les nouveaux éléments, au lieu de re-calculer les 130k existants const mergedRows = [...rows, ...newRows]; newRows.forEach(r => rowsIndex.set(r.codein, r)); set({ rows: mergedRows, rowsIndex, summary: computeSummary(mergedRows, draftChanges, activeMagasin) }); }, setDraftGamme: (codein, gamme) => { const { rows, rowsIndex, draftChanges: oldDrafts } = get(); // Lookup O(1) via l'index au lieu de O(n) via .find() const originalRow = rowsIndex.get(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, summary: computeSummary(rows, draftChanges, get().activeMagasin) }); }, resetDrafts: () => { set({ draftChanges: {}, summary: computeSummary(get().rows, {}, get().activeMagasin) }); }, clearDrafts: (codeins) => { const draftChanges = { ...get().draftChanges }; codeins.forEach((id) => delete draftChanges[id]); set({ draftChanges, summary: computeSummary(get().rows, draftChanges, get().activeMagasin) }); }, /** Apply saved drafts onto row.codeGamme so isModified works after clearing drafts */ applyDraftsToRows: (draftsToApply: Record) => { const rows = get().rows.map(r => { const newGamme = draftsToApply[r.codein]; if (newGamme !== undefined) { return { ...r, codeGamme: newGamme }; } return r; }); set({ rows }); }, setFilter: (key, value) => { set((state) => ({ ...state, filters: { ...state.filters, [key]: value } })); }, 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, rowsIndex, draftChanges: oldDrafts } = get(); const updatedDrafts = { ...oldDrafts }; Object.entries(newChanges).forEach(([codein, gamme]) => { // Lookup O(1) via l'index au lieu de O(n) via .find() const originalRow = rowsIndex.get(codein); const originalGamme = originalRow?.codeGamme; if (gamme === originalGamme) { delete updatedDrafts[codein]; } else { updatedDrafts[codein] = gamme; } }); set({ draftChanges: updatedDrafts, summary: computeSummary(rows, updatedDrafts, get().activeMagasin) }); }, 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, }), } ) );