feat: implement infinite scrolling and background data loading for the product grid component

This commit is contained in:
Michael committed 2026-04-02 17:47:10 +02:00
1 parent f70249555e
commit 4dfdcc1144
4 files changed
+47 -10

No files matched your search

+3
View File
@@ -17,6 +17,8 @@ export async function GET(request: NextRequest) {
const page = parseInt(searchParams.get("page") ?? "0", 10); const page = parseInt(searchParams.get("page") ?? "0", 10);
const pageSize = parseInt(searchParams.get("pageSize") ?? "200", 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 search = searchParams.get("search") ?? undefined;
const gamme = searchParams.get("gamme") ?? undefined; const gamme = searchParams.get("gamme") ?? undefined;
const code3 = searchParams.get("code3") ?? undefined; const code3 = searchParams.get("code3") ?? undefined;
@@ -26,6 +28,7 @@ export async function GET(request: NextRequest) {
codeFournisseur, codeFournisseur,
page: isNaN(page) ? 0 : page, page: isNaN(page) ? 0 : page,
pageSize: isNaN(pageSize) ? 200 : pageSize, pageSize: isNaN(pageSize) ? 200 : pageSize,
offset: offset !== undefined && !isNaN(offset) ? offset : undefined,
search, search,
gamme: gamme || null, gamme: gamme || null,
code3: code3 || null, code3: code3 || null,
+13 -4
View File
@@ -25,6 +25,7 @@ export interface GetProductRowsInput {
filters?: Partial<GridFilters>; filters?: Partial<GridFilters>;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
offset?: number;
search?: string; search?: string;
gamme?: string | null; gamme?: string | null;
code3?: string | null; code3?: string | null;
@@ -317,7 +318,7 @@ async function _fetchAllProductRows(codeFournisseur: string): Promise<ProductRow
// ─── Phase 10 : Filtrer gamme Y sans ventes + compute scores ───────── // ─── Phase 10 : Filtrer gamme Y sans ventes + compute scores ─────────
const allRows = Array.from(productMap.values()); const allRows = Array.from(productMap.values());
const rows = allRows.filter(p => 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; const excludedY = allRows.length - rows.length;
console.log(`[getProductRows] ${rows.length} produits (${excludedY} gamme Y sans ventes exclus), ${mensuelByCodein.size} avec ventes`); 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<GetPro
codeFournisseur, codeFournisseur,
page, page,
pageSize = 200, pageSize = 200,
offset,
search, search,
gamme, gamme,
code3, code3,
@@ -379,9 +381,16 @@ export async function getProductRows(input: GetProductRowsInput): Promise<GetPro
const safePageSize = Math.max(1, Math.min(pageSize, 20000)); const safePageSize = Math.max(1, Math.min(pageSize, 20000));
const totalPages = Math.ceil(total / safePageSize); const totalPages = Math.ceil(total / safePageSize);
const safePage = Math.max(0, Math.min(page, totalPages - 1)); const safePage = Math.max(0, Math.min(page ?? 0, Math.max(0, totalPages - 1)));
const start = safePage * safePageSize;
const rows = filtered.slice(start, start + safePageSize); // Priorité à l'offset s'il est fourni (pour la pagination côté client qui ne respecte pas les blocs de 10k au départ)
const startIdx = offset !== undefined ? offset : safePage * safePageSize;
if (startIdx >= total) {
return { rows: [], total, page: safePage, pageSize: safePageSize, totalPages };
}
const rows = filtered.slice(startIdx, startIdx + safePageSize);
return { rows, total, page: safePage, pageSize: safePageSize, totalPages }; return { rows, total, page: safePage, pageSize: safePageSize, totalPages };
} }
+10 -2
View File
@@ -10,7 +10,7 @@ import { useGridStore } from "@/features/grid/store/use-grid-store";
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts"; import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
import { useInfiniteGrid } from "@/features/grid/hooks/use-infinite-grid"; import { useInfiniteGrid } from "@/features/grid/hooks/use-infinite-grid";
import type { ProductRow } from "@/types/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 { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
@@ -50,7 +50,7 @@ export function GridClient({ initialRows, initialTotal, codeFournisseur, nomFour
appendRows(newRows); appendRows(newRows);
}, [appendRows]); }, [appendRows]);
const { total, isLoadingMore, hasMore, loadNextPage, progress } = useInfiniteGrid({ const { total, isLoadingMore, hasMore, loadNextPage, stopLoading, progress } = useInfiniteGrid({
codeFournisseur, codeFournisseur,
initialTotal, initialTotal,
pageSize: 10000, pageSize: 10000,
@@ -164,6 +164,14 @@ export function GridClient({ initialRows, initialTotal, codeFournisseur, nomFour
style={{ width: `${progress}%`, background: "var(--accent)" }} style={{ width: `${progress}%`, background: "var(--accent)" }}
/> />
</div> </div>
<button
onClick={stopLoading}
className="ml-2 flex items-center gap-1 px-2 py-0.5 rounded-full hover:bg-red-500/10 text-red-600 transition-colors"
title="Arrêter le téléchargement du reste des références"
>
<StopCircle className="w-3.5 h-3.5" />
<span className="text-[10px] uppercase font-bold">Stop</span>
</button>
</div> </div>
)} )}
+21 -4
View File
@@ -18,6 +18,7 @@ export interface InfiniteGridState {
isLoadingMore: boolean; isLoadingMore: boolean;
hasMore: boolean; hasMore: boolean;
loadNextPage: () => void; loadNextPage: () => void;
stopLoading: () => void;
reset: () => void; reset: () => void;
progress: number; progress: number;
loadedPagesCount: number; loadedPagesCount: number;
@@ -35,6 +36,7 @@ export function useInfiniteGrid({
const [total, setTotal] = useState(initialTotal); const [total, setTotal] = useState(initialTotal);
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false);
const [isStopped, setIsStopped] = useState(false);
const [loadedPagesCount, setLoadedPagesCount] = useState(1); // Page 0 est initialRows const [loadedPagesCount, setLoadedPagesCount] = useState(1); // Page 0 est initialRows
const fetchIdRef = useRef(0); const fetchIdRef = useRef(0);
@@ -48,6 +50,11 @@ export function useInfiniteGrid({
page: String(page), page: String(page),
pageSize: String(pageSize), pageSize: String(pageSize),
}); });
if (page > 0) {
params.set("offset", String(200 + (page - 1) * pageSize));
}
if (search) params.set("search", search); if (search) params.set("search", search);
if (gamme) params.set("gamme", gamme); if (gamme) params.set("gamme", gamme);
if (code3) params.set("code3", code3); if (code3) params.set("code3", code3);
@@ -60,12 +67,13 @@ export function useInfiniteGrid({
setTotal(initialTotal); setTotal(initialTotal);
setCurrentPage(0); setCurrentPage(0);
setLoadedPagesCount(1); setLoadedPagesCount(1);
setIsStopped(false);
loadedPagesRef.current = new Set([0]); loadedPagesRef.current = new Set([0]);
fetchIdRef.current += 1; fetchIdRef.current += 1;
}, [codeFournisseur, initialTotal, search, gamme, code3]); }, [codeFournisseur, initialTotal, search, gamme, code3]);
const loadNextPage = useCallback(async () => { const loadNextPage = useCallback(async () => {
if (!codeFournisseur || isLoadingMore) return; if (!codeFournisseur || isLoadingMore || isStopped) return;
const nextPage = currentPage + 1; const nextPage = currentPage + 1;
const alreadyLoaded = loadedPagesRef.current.has(nextPage); const alreadyLoaded = loadedPagesRef.current.has(nextPage);
@@ -102,13 +110,19 @@ export function useInfiniteGrid({
// Envoyer directement au Store Zustand pour bypasser le cycle React DOM // Envoyer directement au Store Zustand pour bypasser le cycle React DOM
onPageLoaded(data.rows); onPageLoaded(data.rows);
} catch (error) { } catch (error) {
if ((error as Error).name === "AbortError") return;
console.error("[useInfiniteGrid] loadNextPage error:", error); console.error("[useInfiniteGrid] loadNextPage error:", error);
} finally { } finally {
if (currentFetchId === fetchIdRef.current) { if (currentFetchId === fetchIdRef.current) {
setIsLoadingMore(false); 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(() => { const reset = useCallback(() => {
fetchIdRef.current += 1; fetchIdRef.current += 1;
@@ -117,10 +131,12 @@ export function useInfiniteGrid({
setCurrentPage(0); setCurrentPage(0);
setLoadedPagesCount(1); setLoadedPagesCount(1);
setIsLoadingMore(false); setIsLoadingMore(false);
setIsStopped(false);
}, [initialTotal]); }, [initialTotal]);
const loadedItemsEstimation = loadedPagesCount * pageSize; // On part du principe qu'il y en a 200 + n * 10 000
const hasMore = loadedItemsEstimation < total && loadedPagesCount < Math.ceil(total / pageSize); const loadedItemsEstimation = 200 + (loadedPagesCount - 1) * pageSize;
const hasMore = !isStopped && loadedItemsEstimation < total;
const progress = total > 0 ? Math.round(Math.min((loadedItemsEstimation / total) * 100, 100)) : 100; const progress = total > 0 ? Math.round(Math.min((loadedItemsEstimation / total) * 100, 100)) : 100;
return { return {
@@ -128,6 +144,7 @@ export function useInfiniteGrid({
isLoadingMore, isLoadingMore,
hasMore, hasMore,
loadNextPage, loadNextPage,
stopLoading,
reset, reset,
progress, progress,
loadedPagesCount, loadedPagesCount,