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 (
);
}
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 */}
{/* 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}
);
}