import { ReactNode, Suspense, useState, useEffect, useMemo, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useLocation } from "wouter"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { LogOut, Store, Menu, AlertTriangle } from "lucide-react"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useIsMobile } from "@/hooks/use-mobile"; import { useScreenSize } from "@/hooks/use-screen-size"; import { StoreProvider } from "@/contexts/StoreContext"; import Sidebar from "./Sidebar"; import ErrorBoundary from "./ErrorBoundary"; import WeatherWidget from "./WeatherWidget"; import DateWidget from "./DateWidget"; import type { Group } from "@shared/schema"; interface LayoutProps { children: ReactNode; } // Tableau vide stable : évite de recréer la valeur du contexte magasin à chaque // rendu tant que /api/groups n'a pas répondu const EMPTY_STORES: Group[] = []; // Affiché dans la zone de contenu pendant le chargement du code d'une page // (le menu et l'en-tête restent visibles) function PageFallback() { return (

Chargement...

); } export default function Layout({ children }: LayoutProps) { const { user } = useAuthUnified(); const [location] = useLocation(); const queryClient = useQueryClient(); const isMobile = useIsMobile(); const { screenSize, isMobileOrTablet, isTablet } = useScreenSize(); const [selectedStoreId, setSelectedStoreId] = useState(() => { // Restaurer le selectedStoreId depuis localStorage si disponible const saved = localStorage.getItem('selectedStoreId'); return saved ? parseInt(saved) : null; }); const [storeInitialized, setStoreInitialized] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { // Restaurer l'état de la sidebar depuis localStorage, mais forcer collapsed sur mobile try { const saved = localStorage.getItem('sidebarCollapsed'); return isMobile ? true : (saved ? JSON.parse(saved) : false); } catch { return isMobile ? true : false; } }); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const { data: stores = EMPTY_STORES } = useQuery({ queryKey: ['/api/groups'], enabled: !!user, }); // Changement de magasin demandé dans le sélecteur : les données dépendantes du // magasin sont invalidées après le rendu (effet ci-dessous) et non dans le // gestionnaire. À ce moment les pages ont déjà basculé sur les clés du nouveau // magasin : les requêtes de l'ancien magasin ne sont plus actives et ne sont // donc plus rechargées pour rien, et cancelRefetch: false réutilise les // requêtes du nouveau magasin déjà en cours. L'invalidation est conservée pour // les requêtes dont la clé ne contient pas le magasin (ex. ['/api/orders'] des // modales de livraison). const storeChangePendingRef = useRef(false); useEffect(() => { if (!storeChangePendingRef.current) return; storeChangePendingRef.current = false; queryClient.invalidateQueries({ predicate: (query) => { const key = query.queryKey; return Boolean(key[0]?.toString().includes('/api/orders') || key[0]?.toString().includes('/api/deliveries') || key[0]?.toString().includes('/api/stats/monthly') || key[0]?.toString().includes('/api/tasks')); } }, { cancelRefetch: false }); }, [selectedStoreId, queryClient]); // Effet pour marquer l'initialisation comme terminée useEffect(() => { if (user && stores.length > 0) { let needsUpdate = false; // Pour tous les rôles, vérifier que selectedStoreId est cohérent avec les stores disponibles if (selectedStoreId) { const storeExists = stores.find(store => store.id === selectedStoreId); if (!storeExists) { setSelectedStoreId(null); localStorage.removeItem('selectedStoreId'); needsUpdate = true; } } // IMPORTANT FIX: Pour les rôles directeur/manager, forcer la sélection d'un magasin spécifique // si aucun n'est sélectionné et qu'ils n'ont accès qu'à un seul magasin if ((user.role === 'directeur' || user.role === 'manager') && !selectedStoreId && stores.length === 1 && !needsUpdate) { const singleStoreId = stores[0].id; setSelectedStoreId(singleStoreId); localStorage.setItem('selectedStoreId', singleStoreId.toString()); } // VALIDATION CRITIQUE: Pour les directeurs/managers, s'assurer qu'un magasin est toujours sélectionné setStoreInitialized(true); } }, [user, stores, selectedStoreId]); const handleLogout = () => { window.location.href = "/api/logout"; }; // Valeur du contexte mémorisée : les consommateurs de useStore() ne sont // re-rendus que lorsqu'une de ces valeurs change réellement const storeContextValue = useMemo(() => ({ selectedStoreId, setSelectedStoreId, stores, sidebarCollapsed, setSidebarCollapsed, mobileMenuOpen, setMobileMenuOpen, storeInitialized }), [selectedStoreId, stores, sidebarCollapsed, mobileMenuOpen, storeInitialized]); return (
{/* Mobile overlay for normal mode */} {isMobile && mobileMenuOpen && (
setMobileMenuOpen(false)} /> )} {/* Sidebar for normal mode */}
{/* Header with mobile menu and store selector */}
{/* Mobile menu button (only in normal mode) */} {isMobile && ( )} {/* Weather and Date widgets - responsive */}
{!isMobile && }
{/* Store selector for admin, directeur, and manager - responsive */} {user && (user.role === 'admin' || user.role === 'directeur' || user.role === 'manager') && stores.length > 0 && (
)}
{/* Alert for directeur/manager without store selection */} {user && (user.role === 'directeur' || user.role === 'manager') && !selectedStoreId && storeInitialized && (

Action requise : Veuillez sélectionner un magasin pour afficher les données. Vos permissions ne permettent pas de voir les données de tous les magasins.

)}
{/* Une erreur dans une page (ou l'échec du chargement de son fichier JS) n'efface plus le menu ; réinitialisée à chaque changement d'URL */} }> {children}
); }