"use client"; import { useEffect, useMemo, useRef, useState, useTransition } from "react"; import { HeatmapGrid } from "@/features/grid/components/heatmap-grid"; import { FloatingSummaryBar } from "@/features/grid/components/floating-summary-bar"; import { BulkActionToolbar } from "@/features/grid/components/bulk-action-toolbar"; import { GridFilterBar } from "@/features/grid/components/grid-filter-bar"; import { ExportDropdown } from "@/features/grid/components/export-dropdown"; import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts"; import type { ProductRow } from "@/types/grid"; import { CheckCircle, AlertCircle, Loader2, RefreshCw } from "lucide-react"; import { useSession } from "next-auth/react"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; import type { GridFilters } from "@/types/grid"; interface GridClientProps { codeFournisseur: string; nomFournisseur: string; fournisseurs: { code: string; nom: string }[]; magasins: { code: string; nom: string }[]; magasin: string; filters: Pick; } type GridRowsStreamMessage = | { type: "start"; loaded: number; total: null } | { type: "chunk"; rows: ProductRow[]; loaded: number; total: number } | { type: "done"; loaded: number; total: number } | { type: "error"; error: string }; /** Petite pastille métrique (libellé + valeur) — densité pro, thème Slate/teal. */ function MetricPill({ label, value, accent = false }: { label: string; value: string; accent?: boolean }) { return (
{label} {value}
); } export function GridClient({ codeFournisseur, nomFournisseur, fournisseurs, magasins, magasin, filters }: GridClientProps) { const { data: session } = useSession(); const isAdmin = (session?.user as { role?: string } | undefined)?.role === "admin"; const setRows = useGridStore((s) => s.setRows); const rows = useGridStore((s) => s.rows); const setActiveGridQuery = useGridStore((s) => s.setActiveGridQuery); const setFilter = useGridStore((s) => s.setFilter); const setCode3Filter = useGridStore((s) => s.setCode3Filter); const setActiveMagasin = useGridStore((s) => s.setActiveMagasin); const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); const [selectedCodeins, setSelectedCodeins] = useState([]); const [isPending, startTransition] = useTransition(); const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); const [isLoadingRows, setIsLoadingRows] = useState(false); const [rowsLoaded, setRowsLoaded] = useState(0); const [totalRows, setTotalRows] = useState(null); const [loadError, setLoadError] = useState(null); const [showStartOverlay, setShowStartOverlay] = useState(false); const visibleCodeins = useMemo(() => rows.map(r => r.codein), [rows]); const { save, hasDrafts, count } = useSaveDrafts(magasin, visibleCodeins); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); // Sync active search params to the store useEffect(() => { if (!isMounted) return; const currentQueryString = searchParams.toString(); // Set the active query, ensuring it starts with ? if not empty setActiveGridQuery(currentQueryString ? `?${currentQueryString}` : ""); }, [searchParams, setActiveGridQuery, isMounted]); // Réinitialiser les filtres UNIQUEMENT quand le fournisseur change const prevFournisseurRef = useRef(null); useEffect(() => { if (!isMounted) return; if (prevFournisseurRef.current !== codeFournisseur) { setFilter("codeGamme", null); setCode3Filter(null); prevFournisseurRef.current = codeFournisseur; } }, [codeFournisseur, setFilter, setCode3Filter, isMounted]); const refreshToken = searchParams.get("_refresh"); useEffect(() => { if (!isMounted) return; const controller = new AbortController(); const params = new URLSearchParams(); params.set("fournisseur", codeFournisseur); params.set("magasin", magasin || "TOTAL"); // `code3` n'est volontairement pas transmis : le serveur ignore ce filtre // (getProductRows ne s'en sert pas) et la Grille l'applique en local. Le // faire voyager relançait un chargement complet à chaque case cochée. if (filters.code1) params.set("code1", filters.code1); if (filters.code2) params.set("code2", filters.code2); if (refreshToken) params.set("refresh", "1"); let accumulatedRows: ProductRow[] = []; setRows([]); setRowsLoaded(0); setTotalRows(null); setLoadError(null); setIsLoadingRows(true); setShowStartOverlay(true); async function loadRows() { try { const response = await fetch(`/api/grid/rows?${params.toString()}`, { signal: controller.signal, cache: "no-store", }); if (!response.ok || !response.body) { throw new Error(`Chargement impossible (${response.status})`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.trim()) continue; const message = JSON.parse(line) as GridRowsStreamMessage; if (message.type === "chunk") { accumulatedRows = accumulatedRows.concat(message.rows); setRows(accumulatedRows); setRowsLoaded(message.loaded); setTotalRows(message.total); } else if (message.type === "done") { setRowsLoaded(message.loaded); setTotalRows(message.total); } else if (message.type === "error") { throw new Error(message.error); } } } } catch (error) { if (!controller.signal.aborted) { setLoadError(error instanceof Error ? error.message : "Erreur de chargement"); } } finally { if (!controller.signal.aborted) { setIsLoadingRows(false); } } } loadRows(); return () => controller.abort(); }, [codeFournisseur, magasin, filters.code1, filters.code2, refreshToken, setRows, isMounted]); // Synchroniser le magasin actif depuis la prop URL (changement de magasin sans rechargement) useEffect(() => { if (!isMounted) return; setActiveMagasin(magasin || "TOTAL"); }, [magasin, setActiveMagasin, isMounted]); const handleSave = () => { startTransition(async () => { const result = await save(); setSaveStatus(result.success ? "success" : "error"); setTimeout(() => setSaveStatus("idle"), 3000); }); }; // Force le rechargement des données depuis le serveur en ignorant le cache // 10 min : on change le paramètre URL `_refresh`, ce qui relance le fetch // avec `refresh=1` (→ forceRefresh) et remonte l'état serveur à jour (INIT). const handleForceRefresh = () => { if (isLoadingRows) return; const params = new URLSearchParams(searchParams.toString()); params.set("_refresh", String(Date.now())); router.replace(`${pathname}?${params.toString()}`); }; if (!isMounted) { return
Initialisation de la grille...
; } const activeStoreNom = magasins.find(m => m.code === magasin)?.nom || "National (Total)"; return (
{/* Header — chrome raffiné (eyebrow teal + fournisseur héro + pastilles métriques) */}
Révision d'assortiment

{nomFournisseur}

{(isLoadingRows || loadError) && (
{isLoadingRows && } {loadError ? loadError : totalRows ? `${rowsLoaded.toLocaleString("fr-FR")} / ${totalRows.toLocaleString("fr-FR")}` : "Chargement…"}
)}
{isAdmin && } {isAdmin && hasDrafts && ( )}
{/* Filters */}
{/* Bulk toolbar (contextual) */}
setSelectedCodeins([])} />
{/* Main content */}
{/* Summary bar */}
{/* Overlay de démarrage premium (Glassmorphism semi-bloquant) */} {showStartOverlay && isLoadingRows && (
{/* Header avec logo ou icône animée */}
{/* Titre et descriptions */}

Flux d'Assortiment en cours

Chargement progressif et en temps réel des données pour {nomFournisseur}.

{/* Barre de progression dynamique */}
Progression {totalRows ? `${Math.round((rowsLoaded / totalRows) * 100)}%` : `${rowsLoaded.toLocaleString("fr-FR")} réfs`}

{totalRows ? `${rowsLoaded.toLocaleString("fr-FR")} sur ${totalRows.toLocaleString("fr-FR")} références chargées` : `${rowsLoaded.toLocaleString("fr-FR")} références récupérées...`}

{/* Boutons d'action */}

Le chargement continuera en arrière-plan sans bloquer votre navigation.

)}
); }