mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
perf(grid): optimize state store with O(1) row indexing and debounced summary calculations
This commit is contained in:
1 parent
8f2c4c77ba
commit
d75102fb18
6 files changed
+86
-37
No files matched your search
@@ -5,7 +5,7 @@ import { useSession } from "next-auth/react";
|
|||||||
import { BulkAiAnalyzer } from "./bulk-ai-analyzer";
|
import { BulkAiAnalyzer } from "./bulk-ai-analyzer";
|
||||||
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
|
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
|
||||||
import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera, ChevronDown } from "lucide-react";
|
import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera, ChevronDown } from "lucide-react";
|
||||||
import { useState, useTransition } from "react";
|
import { useMemo, useState, useTransition } from "react";
|
||||||
import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
|
import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
|
||||||
import { SuccessModal } from "@/components/shared/success-modal";
|
import { SuccessModal } from "@/components/shared/success-modal";
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +45,7 @@ export function FloatingSummaryBar() {
|
|||||||
message: ""
|
message: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleCodeins = rows.map(r => r.codein);
|
const visibleCodeins = useMemo(() => rows.map(r => r.codein), [rows]);
|
||||||
const { save, hasDrafts, count } = useSaveDrafts(filters.magasin || "TOTAL", visibleCodeins);
|
const { save, hasDrafts, count } = useSaveDrafts(filters.magasin || "TOTAL", visibleCodeins);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ interface GridFilterBarProps {
|
|||||||
magasins: { code: string; nom: string }[];
|
magasins: { code: string; nom: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_DRAFT_CHANGES: Record<string, string> = {};
|
||||||
|
|
||||||
export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
||||||
const filters = useGridStore((s) => s.filters);
|
const filters = useGridStore((s) => s.filters);
|
||||||
const setFilter = useGridStore((s) => s.setFilter);
|
const setFilter = useGridStore((s) => s.setFilter);
|
||||||
const rows = useGridStore((s) => s.rows);
|
const rows = useGridStore((s) => s.rows);
|
||||||
const draftChanges = useGridStore((s) => s.draftChanges);
|
const draftChanges = useGridStore((s) => s.filters.codeGamme ? s.draftChanges : EMPTY_DRAFT_CHANGES);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ interface HeatmapGridProps {
|
|||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_DRAFT_CHANGES: Record<string, GammeCode> = {};
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OPTIMISATION PERFORMANCES (React.memo + Zustand Selectors granulaires)
|
// OPTIMISATION PERFORMANCES (React.memo + Zustand Selectors granulaires)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -272,7 +274,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
|||||||
const rows = useGridStore((s) => s.rows);
|
const rows = useGridStore((s) => s.rows);
|
||||||
const filters = useGridStore((s) => s.filters);
|
const filters = useGridStore((s) => s.filters);
|
||||||
const displayDensity = useGridStore((s) => s.displayDensity);
|
const displayDensity = useGridStore((s) => s.displayDensity);
|
||||||
const draftChanges = useGridStore((s) => s.draftChanges);
|
const draftChanges = useGridStore((s) => s.filters.codeGamme ? s.draftChanges : EMPTY_DRAFT_CHANGES);
|
||||||
const activeMagasin = useGridStore((s) => s.activeMagasin);
|
const activeMagasin = useGridStore((s) => s.activeMagasin);
|
||||||
|
|
||||||
// Filtre client-side par code3 (famille) et codeGamme
|
// Filtre client-side par code3 (famille) et codeGamme
|
||||||
@@ -707,7 +709,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
|||||||
count: tableRows.length,
|
count: tableRows.length,
|
||||||
getScrollElement: () => tableContainerRef.current,
|
getScrollElement: () => tableContainerRef.current,
|
||||||
estimateSize: () => rowHeight,
|
estimateSize: () => rowHeight,
|
||||||
overscan: 20,
|
overscan: 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleColumns = table.getVisibleLeafColumns();
|
const visibleColumns = table.getVisibleLeafColumns();
|
||||||
@@ -769,7 +771,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={tableContainerRef}
|
ref={tableContainerRef}
|
||||||
className="h-full w-full overflow-auto rounded-[12px] scroll-smooth relative"
|
className="h-full w-full overflow-auto rounded-[12px] relative"
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-surface)",
|
background: "var(--bg-surface)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
@@ -835,7 +837,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
|||||||
const isSelected = row.getIsSelected();
|
const isSelected = row.getIsSelected();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={row.id} ref={rowVirtualizer.measureElement} style={{ position: 'absolute', top: 0, left: 0, width: '100%' }}>
|
<div key={row.id} style={{ position: 'absolute', top: 0, left: 0, width: '100%' }}>
|
||||||
<GridRow
|
<GridRow
|
||||||
virtualRow={virtualRow}
|
virtualRow={virtualRow}
|
||||||
row={row}
|
row={row}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
||||||
import { saveDraftChanges } from "@/features/grid/api/save-draft-changes";
|
import { saveDraftChanges } from "@/features/grid/api/save-draft-changes";
|
||||||
import { GammeCode } from "@/types/grid";
|
import { GammeCode } from "@/types/grid";
|
||||||
@@ -17,9 +17,16 @@ export function useSaveDrafts(magasin: string, filterCodeins?: string[]) {
|
|||||||
const applyDraftsToRows = useGridStore((s) => s.applyDraftsToRows);
|
const applyDraftsToRows = useGridStore((s) => s.applyDraftsToRows);
|
||||||
|
|
||||||
// Only consider changes that are in the filter list (if provided)
|
// Only consider changes that are in the filter list (if provided)
|
||||||
const activeDrafts = filterCodeins
|
const filterCodeinSet = useMemo(
|
||||||
? Object.fromEntries(Object.entries(draftChanges).filter(([codein]) => filterCodeins.includes(codein)))
|
() => filterCodeins ? new Set(filterCodeins) : null,
|
||||||
: draftChanges;
|
[filterCodeins]
|
||||||
|
);
|
||||||
|
const activeDrafts = useMemo(
|
||||||
|
() => filterCodeinSet
|
||||||
|
? Object.fromEntries(Object.entries(draftChanges).filter(([codein]) => filterCodeinSet.has(codein)))
|
||||||
|
: draftChanges,
|
||||||
|
[draftChanges, filterCodeinSet]
|
||||||
|
);
|
||||||
|
|
||||||
const count = Object.keys(activeDrafts).length;
|
const count = Object.keys(activeDrafts).length;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type { GammeCode, GridFilters, GridSummary, ProductRow } from "@/types/gr
|
|||||||
interface GridState {
|
interface GridState {
|
||||||
/** Source data from server */
|
/** Source data from server */
|
||||||
rows: ProductRow[];
|
rows: ProductRow[];
|
||||||
|
/** Fast lookup by codein; not persisted */
|
||||||
|
rowsByCodein: Record<string, ProductRow>;
|
||||||
/** Draft edits: codein → new GammeCode */
|
/** Draft edits: codein → new GammeCode */
|
||||||
draftChanges: Record<string, GammeCode>;
|
draftChanges: Record<string, GammeCode>;
|
||||||
filters: GridFilters;
|
filters: GridFilters;
|
||||||
@@ -40,6 +42,14 @@ interface GridState {
|
|||||||
setColumnSizing: (updater: Record<string, number> | ((old: Record<string, number>) => Record<string, number>)) => 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 {
|
function computeSummary(rows: ProductRow[], drafts: Record<string, GammeCode>, magasin = "TOTAL"): GridSummary {
|
||||||
const active = rows.filter((r) => {
|
const active = rows.filter((r) => {
|
||||||
const g = drafts[r.codein] ?? r.codeGamme;
|
const g = drafts[r.codein] ?? r.codeGamme;
|
||||||
@@ -64,6 +74,7 @@ export const useGridStore = create<GridState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
rows: [],
|
rows: [],
|
||||||
|
rowsByCodein: {},
|
||||||
activeMagasin: "TOTAL",
|
activeMagasin: "TOTAL",
|
||||||
draftChanges: {},
|
draftChanges: {},
|
||||||
filters: {
|
filters: {
|
||||||
@@ -89,17 +100,26 @@ export const useGridStore = create<GridState>()(
|
|||||||
columnSizing: {},
|
columnSizing: {},
|
||||||
|
|
||||||
setActiveMagasin: (code) => {
|
setActiveMagasin: (code) => {
|
||||||
const { rows, draftChanges } = get();
|
set({ activeMagasin: code });
|
||||||
set({ activeMagasin: code, summary: computeSummary(rows, draftChanges, code) });
|
if (summaryTimer) clearTimeout(summaryTimer);
|
||||||
|
summaryTimer = setTimeout(() => {
|
||||||
|
const state = get();
|
||||||
|
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
|
||||||
|
}, 80);
|
||||||
},
|
},
|
||||||
|
|
||||||
setRows: (rows) => {
|
setRows: (rows) => {
|
||||||
set({ rows, summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
|
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) => {
|
setDraftGamme: (codein, gamme) => {
|
||||||
const { rows, draftChanges: oldDrafts } = get();
|
const { rowsByCodein, draftChanges: oldDrafts } = get();
|
||||||
const originalRow = rows.find(r => r.codein === codein);
|
const originalRow = rowsByCodein[codein];
|
||||||
const originalGamme = originalRow?.codeGamme;
|
const originalGamme = originalRow?.codeGamme;
|
||||||
|
|
||||||
const draftChanges = { ...oldDrafts };
|
const draftChanges = { ...oldDrafts };
|
||||||
@@ -112,16 +132,31 @@ export const useGridStore = create<GridState>()(
|
|||||||
draftChanges[codein] = gamme;
|
draftChanges[codein] = gamme;
|
||||||
}
|
}
|
||||||
|
|
||||||
set({ draftChanges, summary: computeSummary(rows, draftChanges, get().activeMagasin) });
|
set({ draftChanges });
|
||||||
|
if (summaryTimer) clearTimeout(summaryTimer);
|
||||||
|
summaryTimer = setTimeout(() => {
|
||||||
|
const state = get();
|
||||||
|
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
|
||||||
|
}, 80);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetDrafts: () => {
|
resetDrafts: () => {
|
||||||
set({ draftChanges: {}, summary: computeSummary(get().rows, {}, get().activeMagasin) });
|
set({ draftChanges: {} });
|
||||||
|
if (summaryTimer) clearTimeout(summaryTimer);
|
||||||
|
summaryTimer = setTimeout(() => {
|
||||||
|
const state = get();
|
||||||
|
set({ summary: computeSummary(state.rows, {}, state.activeMagasin) });
|
||||||
|
}, 80);
|
||||||
},
|
},
|
||||||
clearDrafts: (codeins) => {
|
clearDrafts: (codeins) => {
|
||||||
const draftChanges = { ...get().draftChanges };
|
const draftChanges = { ...get().draftChanges };
|
||||||
codeins.forEach((id) => delete draftChanges[id]);
|
codeins.forEach((id) => delete draftChanges[id]);
|
||||||
set({ draftChanges, summary: computeSummary(get().rows, draftChanges, get().activeMagasin) });
|
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 */
|
/** Apply saved drafts onto row.codeGamme so isModified works after clearing drafts */
|
||||||
applyDraftsToRows: (draftsToApply: Record<string, GammeCode>) => {
|
applyDraftsToRows: (draftsToApply: Record<string, GammeCode>) => {
|
||||||
@@ -132,7 +167,7 @@ export const useGridStore = create<GridState>()(
|
|||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
set({ rows });
|
set({ rows, rowsByCodein: indexRows(rows), summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
|
||||||
},
|
},
|
||||||
setFilter: (key, value) => {
|
setFilter: (key, value) => {
|
||||||
set((state) => ({ ...state, filters: { ...state.filters, [key]: value } }));
|
set((state) => ({ ...state, filters: { ...state.filters, [key]: value } }));
|
||||||
@@ -144,11 +179,11 @@ export const useGridStore = create<GridState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
batchSetDraftGamme: (newChanges) => {
|
batchSetDraftGamme: (newChanges) => {
|
||||||
const { rows, draftChanges: oldDrafts } = get();
|
const { rows, rowsByCodein, draftChanges: oldDrafts } = get();
|
||||||
const updatedDrafts = { ...oldDrafts };
|
const updatedDrafts = { ...oldDrafts };
|
||||||
|
|
||||||
Object.entries(newChanges).forEach(([codein, gamme]) => {
|
Object.entries(newChanges).forEach(([codein, gamme]) => {
|
||||||
const originalRow = rows.find(r => r.codein === codein);
|
const originalRow = rowsByCodein[codein];
|
||||||
const originalGamme = originalRow?.codeGamme;
|
const originalGamme = originalRow?.codeGamme;
|
||||||
|
|
||||||
if (gamme === originalGamme) {
|
if (gamme === originalGamme) {
|
||||||
@@ -158,7 +193,12 @@ export const useGridStore = create<GridState>()(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
set({ draftChanges: updatedDrafts, summary: computeSummary(rows, updatedDrafts, get().activeMagasin) });
|
set({ draftChanges: updatedDrafts });
|
||||||
|
if (summaryTimer) clearTimeout(summaryTimer);
|
||||||
|
summaryTimer = setTimeout(() => {
|
||||||
|
const state = get();
|
||||||
|
set({ summary: computeSummary(rows, state.draftChanges, state.activeMagasin) });
|
||||||
|
}, 80);
|
||||||
},
|
},
|
||||||
|
|
||||||
setColumnVisibility: (updater) => {
|
setColumnVisibility: (updater) => {
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
# Plan - Commit et Push des modifications locales
|
# Plan - Commit et Push des optimisations de performances de la grille
|
||||||
|
|
||||||
## Contexte
|
## Contexte
|
||||||
L'utilisateur a demandé de commiter et pusher ses modifications locales actuelles. L'analyse montre qu'il s'agit d'une implémentation majeure du chargement en streaming pour la grille de produits (11 fichiers modifiés et 2 fichiers non suivis).
|
L'utilisateur a demandé de commiter et pusher de nouvelles modifications locales. L'analyse révèle des optimisations de performances clés apportées au store Zustand de la grille :
|
||||||
|
1. Indexation rapide des lignes de produits par `codein` dans `rowsByCodein` pour accélérer la mise à jour des brouillons de O(N) à O(1).
|
||||||
|
2. Debouncing du calcul des résumés (`computeSummary`) afin d'alléger le thread principal lors des imports en streaming ou des modifications de masse.
|
||||||
|
|
||||||
## Focus Actuel
|
## Focus Actuel
|
||||||
Validation finale et dépôt propre.
|
Validation de la compilation, staging des optimisations de performance, commit et push.
|
||||||
|
|
||||||
## Master Plan
|
## Master Plan
|
||||||
- [x] Analyser l'état actuel de Git et identifier les fichiers modifiés/ajoutés (Fait)
|
- [x] Analyser l'état de Git et identifier les nouveaux fichiers modifiés (Fait: 5 fichiers)
|
||||||
- [x] Vérifier la compilation globale avec `rtk tsc` pour s'assurer que tout est propre (Fait)
|
- [ ] Vérifier la compilation globale du projet avec `rtk tsc`
|
||||||
- [x] Ajouter les fichiers à l'index Git (`rtk git add`) (Fait)
|
- [ ] Ajouter les fichiers à l'index Git (`rtk git add`)
|
||||||
- [x] Créer le commit avec un message descriptif et professionnel (`rtk git commit`) (Fait: "feat(grid): implement product streaming with JSON chunks for grid client")
|
- [ ] Créer le commit avec un message décrivant les optimisations de performance (`rtk git commit`)
|
||||||
- [x] Envoyer les modifications sur la branche distante (`rtk git push`) (Fait)
|
- [ ] Envoyer le commit vers le dépôt distant (`rtk git push`)
|
||||||
- [x] Valider le statut Git final (Fait)
|
- [ ] Valider le statut Git final
|
||||||
|
|
||||||
## Progress Log
|
## Progress Log
|
||||||
- **2026-05-26 13:31** : Analyse initiale des modifications. Identification de l'implémentation de la grille en streaming (`src/features/grid` et `/api/grid`). Initialisation du plan de commit et push.
|
- **2026-05-26 13:53** : Analyse des nouvelles modifications. Découverte des optimisations majeures sur `use-grid-store.ts` (indexation O(1) et debouncing des calculs de résumé).
|
||||||
- **2026-05-26 13:32** : Vérification de la compilation effectuée via `rtk tsc`. Aucun problème introduit dans les fichiers de la grille.
|
|
||||||
- **2026-05-26 13:33** : Ajout de tous les fichiers modifiés et nouveaux à l'index Git via `rtk git add .`.
|
|
||||||
- **2026-05-26 13:34** : Création du commit avec le message descriptif sur le streaming de la grille.
|
|
||||||
- **2026-05-26 13:35** : Push réussi des modifications vers la branche distante `origin/main`.
|
|
||||||
Reference in new issue
Block a user