From 4dfdcc114470eb383155a6ad252f3ba188e2ff8d Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 2 Apr 2026 17:47:10 +0200 Subject: [PATCH] feat: implement infinite scrolling and background data loading for the product grid component --- src/app/api/grid/rows/route.ts | 3 +++ src/features/grid/api/get-product-rows.ts | 17 +++++++++---- src/features/grid/components/grid-client.tsx | 12 ++++++++-- src/features/grid/hooks/use-infinite-grid.ts | 25 ++++++++++++++++---- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/app/api/grid/rows/route.ts b/src/app/api/grid/rows/route.ts index 1da854c..53e7478 100644 --- a/src/app/api/grid/rows/route.ts +++ b/src/app/api/grid/rows/route.ts @@ -17,6 +17,8 @@ export async function GET(request: NextRequest) { const page = parseInt(searchParams.get("page") ?? "0", 10); const pageSize = parseInt(searchParams.get("pageSize") ?? "200", 10); + const offsetRaw = searchParams.get("offset"); + const offset = offsetRaw ? parseInt(offsetRaw, 10) : undefined; const search = searchParams.get("search") ?? undefined; const gamme = searchParams.get("gamme") ?? undefined; const code3 = searchParams.get("code3") ?? undefined; @@ -26,6 +28,7 @@ export async function GET(request: NextRequest) { codeFournisseur, page: isNaN(page) ? 0 : page, pageSize: isNaN(pageSize) ? 200 : pageSize, + offset: offset !== undefined && !isNaN(offset) ? offset : undefined, search, gamme: gamme || null, code3: code3 || null, diff --git a/src/features/grid/api/get-product-rows.ts b/src/features/grid/api/get-product-rows.ts index aa0547c..2f35b4d 100644 --- a/src/features/grid/api/get-product-rows.ts +++ b/src/features/grid/api/get-product-rows.ts @@ -25,6 +25,7 @@ export interface GetProductRowsInput { filters?: Partial; page?: number; pageSize?: number; + offset?: number; search?: string; gamme?: string | null; code3?: string | null; @@ -317,7 +318,7 @@ async function _fetchAllProductRows(codeFournisseur: string): Promise p.codeGamme !== "Y" || p.totalQuantite > 0); + const rows = allRows.filter(p => p.codeGamme?.trim().toUpperCase() !== "Y" || p.totalQuantite > 0); const excludedY = allRows.length - rows.length; console.log(`[getProductRows] ${rows.length} produits (${excludedY} gamme Y sans ventes exclus), ${mensuelByCodein.size} avec ventes`); @@ -339,6 +340,7 @@ export async function getProductRows(input: GetProductRowsInput): Promise= total) { + return { rows: [], total, page: safePage, pageSize: safePageSize, totalPages }; + } + + const rows = filtered.slice(startIdx, startIdx + safePageSize); return { rows, total, page: safePage, pageSize: safePageSize, totalPages }; } diff --git a/src/features/grid/components/grid-client.tsx b/src/features/grid/components/grid-client.tsx index 0d5d1c6..0376340 100644 --- a/src/features/grid/components/grid-client.tsx +++ b/src/features/grid/components/grid-client.tsx @@ -10,7 +10,7 @@ import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts"; import { useInfiniteGrid } from "@/features/grid/hooks/use-infinite-grid"; import type { ProductRow } from "@/types/grid"; -import { CheckCircle, AlertCircle, Loader2 } from "lucide-react"; +import { CheckCircle, AlertCircle, Loader2, StopCircle } from "lucide-react"; import { useSession } from "next-auth/react"; import { useSearchParams } from "next/navigation"; @@ -50,7 +50,7 @@ export function GridClient({ initialRows, initialTotal, codeFournisseur, nomFour appendRows(newRows); }, [appendRows]); - const { total, isLoadingMore, hasMore, loadNextPage, progress } = useInfiniteGrid({ + const { total, isLoadingMore, hasMore, loadNextPage, stopLoading, progress } = useInfiniteGrid({ codeFournisseur, initialTotal, pageSize: 10000, @@ -164,6 +164,14 @@ export function GridClient({ initialRows, initialTotal, codeFournisseur, nomFour style={{ width: `${progress}%`, background: "var(--accent)" }} /> + )} diff --git a/src/features/grid/hooks/use-infinite-grid.ts b/src/features/grid/hooks/use-infinite-grid.ts index 0c61db3..df7b8d1 100644 --- a/src/features/grid/hooks/use-infinite-grid.ts +++ b/src/features/grid/hooks/use-infinite-grid.ts @@ -18,6 +18,7 @@ export interface InfiniteGridState { isLoadingMore: boolean; hasMore: boolean; loadNextPage: () => void; + stopLoading: () => void; reset: () => void; progress: number; loadedPagesCount: number; @@ -35,6 +36,7 @@ export function useInfiniteGrid({ const [total, setTotal] = useState(initialTotal); const [currentPage, setCurrentPage] = useState(0); const [isLoadingMore, setIsLoadingMore] = useState(false); + const [isStopped, setIsStopped] = useState(false); const [loadedPagesCount, setLoadedPagesCount] = useState(1); // Page 0 est initialRows const fetchIdRef = useRef(0); @@ -48,6 +50,11 @@ export function useInfiniteGrid({ page: String(page), pageSize: String(pageSize), }); + + if (page > 0) { + params.set("offset", String(200 + (page - 1) * pageSize)); + } + if (search) params.set("search", search); if (gamme) params.set("gamme", gamme); if (code3) params.set("code3", code3); @@ -60,12 +67,13 @@ export function useInfiniteGrid({ setTotal(initialTotal); setCurrentPage(0); setLoadedPagesCount(1); + setIsStopped(false); loadedPagesRef.current = new Set([0]); fetchIdRef.current += 1; }, [codeFournisseur, initialTotal, search, gamme, code3]); const loadNextPage = useCallback(async () => { - if (!codeFournisseur || isLoadingMore) return; + if (!codeFournisseur || isLoadingMore || isStopped) return; const nextPage = currentPage + 1; const alreadyLoaded = loadedPagesRef.current.has(nextPage); @@ -102,13 +110,19 @@ export function useInfiniteGrid({ // Envoyer directement au Store Zustand pour bypasser le cycle React DOM onPageLoaded(data.rows); } catch (error) { + if ((error as Error).name === "AbortError") return; console.error("[useInfiniteGrid] loadNextPage error:", error); } finally { if (currentFetchId === fetchIdRef.current) { setIsLoadingMore(false); } } - }, [codeFournisseur, currentPage, total, pageSize, isLoadingMore, buildUrl, onPageLoaded]); + }, [codeFournisseur, currentPage, total, pageSize, isLoadingMore, isStopped, buildUrl, onPageLoaded]); + + const stopLoading = useCallback(() => { + setIsStopped(true); + setIsLoadingMore(false); + }, []); const reset = useCallback(() => { fetchIdRef.current += 1; @@ -117,10 +131,12 @@ export function useInfiniteGrid({ setCurrentPage(0); setLoadedPagesCount(1); setIsLoadingMore(false); + setIsStopped(false); }, [initialTotal]); - const loadedItemsEstimation = loadedPagesCount * pageSize; - const hasMore = loadedItemsEstimation < total && loadedPagesCount < Math.ceil(total / pageSize); + // On part du principe qu'il y en a 200 + n * 10 000 + const loadedItemsEstimation = 200 + (loadedPagesCount - 1) * pageSize; + const hasMore = !isStopped && loadedItemsEstimation < total; const progress = total > 0 ? Math.round(Math.min((loadedItemsEstimation / total) * 100, 100)) : 100; return { @@ -128,6 +144,7 @@ export function useInfiniteGrid({ isLoadingMore, hasMore, loadNextPage, + stopLoading, reset, progress, loadedPagesCount,