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 { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
|
||||
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 { SuccessModal } from "@/components/shared/success-modal";
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ export function FloatingSummaryBar() {
|
||||
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 handleSave = () => {
|
||||
|
||||
@@ -15,11 +15,13 @@ interface GridFilterBarProps {
|
||||
magasins: { code: string; nom: string }[];
|
||||
}
|
||||
|
||||
const EMPTY_DRAFT_CHANGES: Record<string, string> = {};
|
||||
|
||||
export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
|
||||
const filters = useGridStore((s) => s.filters);
|
||||
const setFilter = useGridStore((s) => s.setFilter);
|
||||
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 searchParams = useSearchParams();
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||
|
||||
@@ -86,6 +86,8 @@ interface HeatmapGridProps {
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_DRAFT_CHANGES: Record<string, GammeCode> = {};
|
||||
|
||||
// =========================================================================
|
||||
// OPTIMISATION PERFORMANCES (React.memo + Zustand Selectors granulaires)
|
||||
// =========================================================================
|
||||
@@ -272,7 +274,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
const rows = useGridStore((s) => s.rows);
|
||||
const filters = useGridStore((s) => s.filters);
|
||||
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);
|
||||
|
||||
// Filtre client-side par code3 (famille) et codeGamme
|
||||
@@ -707,7 +709,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
count: tableRows.length,
|
||||
getScrollElement: () => tableContainerRef.current,
|
||||
estimateSize: () => rowHeight,
|
||||
overscan: 20,
|
||||
overscan: 8,
|
||||
});
|
||||
|
||||
const visibleColumns = table.getVisibleLeafColumns();
|
||||
@@ -769,7 +771,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
|
||||
<div
|
||||
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={{
|
||||
background: "var(--bg-surface)",
|
||||
border: "1px solid var(--border)",
|
||||
@@ -835,7 +837,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
const isSelected = row.getIsSelected();
|
||||
|
||||
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
|
||||
virtualRow={virtualRow}
|
||||
row={row}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
||||
import { saveDraftChanges } from "@/features/grid/api/save-draft-changes";
|
||||
import { GammeCode } from "@/types/grid";
|
||||
@@ -17,9 +17,16 @@ export function useSaveDrafts(magasin: string, filterCodeins?: string[]) {
|
||||
const applyDraftsToRows = useGridStore((s) => s.applyDraftsToRows);
|
||||
|
||||
// Only consider changes that are in the filter list (if provided)
|
||||
const activeDrafts = filterCodeins
|
||||
? Object.fromEntries(Object.entries(draftChanges).filter(([codein]) => filterCodeins.includes(codein)))
|
||||
: draftChanges;
|
||||
const filterCodeinSet = useMemo(
|
||||
() => filterCodeins ? new Set(filterCodeins) : null,
|
||||
[filterCodeins]
|
||||
);
|
||||
const activeDrafts = useMemo(
|
||||
() => filterCodeinSet
|
||||
? Object.fromEntries(Object.entries(draftChanges).filter(([codein]) => filterCodeinSet.has(codein)))
|
||||
: draftChanges,
|
||||
[draftChanges, filterCodeinSet]
|
||||
);
|
||||
|
||||
const count = Object.keys(activeDrafts).length;
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { GammeCode, GridFilters, GridSummary, ProductRow } from "@/types/gr
|
||||
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;
|
||||
@@ -40,6 +42,14 @@ interface GridState {
|
||||
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;
|
||||
@@ -64,6 +74,7 @@ export const useGridStore = create<GridState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
rows: [],
|
||||
rowsByCodein: {},
|
||||
activeMagasin: "TOTAL",
|
||||
draftChanges: {},
|
||||
filters: {
|
||||
@@ -89,17 +100,26 @@ export const useGridStore = create<GridState>()(
|
||||
columnSizing: {},
|
||||
|
||||
setActiveMagasin: (code) => {
|
||||
const { rows, draftChanges } = get();
|
||||
set({ activeMagasin: code, summary: computeSummary(rows, draftChanges, 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, 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) => {
|
||||
const { rows, draftChanges: oldDrafts } = get();
|
||||
const originalRow = rows.find(r => r.codein === codein);
|
||||
const { rowsByCodein, draftChanges: oldDrafts } = get();
|
||||
const originalRow = rowsByCodein[codein];
|
||||
const originalGamme = originalRow?.codeGamme;
|
||||
|
||||
const draftChanges = { ...oldDrafts };
|
||||
@@ -112,16 +132,31 @@ export const useGridStore = create<GridState>()(
|
||||
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: () => {
|
||||
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) => {
|
||||
const draftChanges = { ...get().draftChanges };
|
||||
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 */
|
||||
applyDraftsToRows: (draftsToApply: Record<string, GammeCode>) => {
|
||||
@@ -132,7 +167,7 @@ export const useGridStore = create<GridState>()(
|
||||
}
|
||||
return r;
|
||||
});
|
||||
set({ rows });
|
||||
set({ rows, rowsByCodein: indexRows(rows), summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
|
||||
},
|
||||
setFilter: (key, value) => {
|
||||
set((state) => ({ ...state, filters: { ...state.filters, [key]: value } }));
|
||||
@@ -144,11 +179,11 @@ export const useGridStore = create<GridState>()(
|
||||
},
|
||||
|
||||
batchSetDraftGamme: (newChanges) => {
|
||||
const { rows, draftChanges: oldDrafts } = get();
|
||||
const { rows, rowsByCodein, draftChanges: oldDrafts } = get();
|
||||
const updatedDrafts = { ...oldDrafts };
|
||||
|
||||
Object.entries(newChanges).forEach(([codein, gamme]) => {
|
||||
const originalRow = rows.find(r => r.codein === codein);
|
||||
const originalRow = rowsByCodein[codein];
|
||||
const originalGamme = originalRow?.codeGamme;
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
# Plan - Commit et Push des modifications locales
|
||||
# Plan - Commit et Push des optimisations de performances de la grille
|
||||
|
||||
## 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
|
||||
Validation finale et dépôt propre.
|
||||
Validation de la compilation, staging des optimisations de performance, commit et push.
|
||||
|
||||
## Master Plan
|
||||
- [x] Analyser l'état actuel de Git et identifier les fichiers modifiés/ajoutés (Fait)
|
||||
- [x] Vérifier la compilation globale avec `rtk tsc` pour s'assurer que tout est propre (Fait)
|
||||
- [x] Ajouter les fichiers à l'index Git (`rtk git add`) (Fait)
|
||||
- [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")
|
||||
- [x] Envoyer les modifications sur la branche distante (`rtk git push`) (Fait)
|
||||
- [x] Valider le statut Git final (Fait)
|
||||
- [x] Analyser l'état de Git et identifier les nouveaux fichiers modifiés (Fait: 5 fichiers)
|
||||
- [ ] Vérifier la compilation globale du projet avec `rtk tsc`
|
||||
- [ ] Ajouter les fichiers à l'index Git (`rtk git add`)
|
||||
- [ ] Créer le commit avec un message décrivant les optimisations de performance (`rtk git commit`)
|
||||
- [ ] Envoyer le commit vers le dépôt distant (`rtk git push`)
|
||||
- [ ] Valider le statut Git final
|
||||
|
||||
## 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: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`.
|
||||
- **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é).
|
||||
Reference in new issue
Block a user