mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: implement infinite scrolling and background data loading for the product grid component
This commit is contained in:
1 parent
f70249555e
commit
4dfdcc1144
4 files changed
+47
-10
No files matched your search
@@ -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,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface GetProductRowsInput {
|
||||
filters?: Partial<GridFilters>;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
gamme?: 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 ─────────
|
||||
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;
|
||||
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,
|
||||
page,
|
||||
pageSize = 200,
|
||||
offset,
|
||||
search,
|
||||
gamme,
|
||||
code3,
|
||||
@@ -379,9 +381,16 @@ export async function getProductRows(input: GetProductRowsInput): Promise<GetPro
|
||||
|
||||
const safePageSize = Math.max(1, Math.min(pageSize, 20000));
|
||||
const totalPages = Math.ceil(total / safePageSize);
|
||||
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
||||
const start = safePage * safePageSize;
|
||||
const rows = filtered.slice(start, start + safePageSize);
|
||||
const safePage = Math.max(0, Math.min(page ?? 0, Math.max(0, totalPages - 1)));
|
||||
|
||||
// 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 };
|
||||
}
|
||||
@@ -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)" }}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user