Files
LogiFlow/client/src/components/Sidebar.tsx
T
Claude 5d3e5357a0 perf(client): pages chargées à la demande et un seul appel /api/user
- Routes en React.lazy (sauf connexion et accueil) avec un Suspense dans
  la zone de contenu ; recharts n'est plus préchargé. JS initial :
  1,72 Mo -> 625 Ko (183 Ko gzip). Rechargement automatique unique si un
  ancien fichier JS a disparu après un déploiement, et ErrorBoundary par
  page pour garder le menu affiché en cas d'erreur.
- Authentification unifiée sur le cache React Query ['/api/user'] : un
  seul GET /api/user pour toute l'application (au lieu de 4 à 5 par
  page), plus de rechargement complet après la connexion.
- Pas de nouvel essai sur les erreurs 4xx ; fournisseurs et magasins en
  cache 5 minutes ; valeur du StoreContext mémoïsée et redimensionnement
  qui ne re-rend que lorsque le palier d'écran change.
- Pages : requêtes inutilisées ou en double supprimées, appels lancés
  seulement une fois l'utilisateur connu, invalidations ciblées au lieu
  de rechargements complets, filtres et tris mémoïsés, anciennes données
  gardées (et estompées) pendant un changement de filtre ou de mois,
  recherche DLC mobile temporisée, vérifications de factures à
  concurrence bornée, navigation interne sans rechargement de la page.
- index.html : script de bannière Replit retiré, lang="fr".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb
2026-10-03 04:29:51 +00:00

925 lines
32 KiB
TypeScript

import { Link, useLocation } from "wouter";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { Button } from "@/components/ui/button";
import { useStore } from "@/contexts/StoreContext";
import { useIsMobile } from "@/hooks/use-mobile";
import { useScreenSize } from "@/hooks/use-screen-size";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient";
import {
Store,
Calendar,
BarChart3,
Package,
Truck,
Building,
Users,
UserCog,
LogOut,
FileText,
Megaphone,
Database,
ShoppingCart,
Clock,
ListTodo,
ChevronLeft,
ChevronRight,
X,
Settings,
Wrench,
FileUp,
Send,
Receipt,
TrendingUp,
CreditCard,
BookUser,
} from "lucide-react";
export default function Sidebar() {
const { user, isLoading, error } = useAuthUnified();
const [location] = useLocation();
const { sidebarCollapsed, setSidebarCollapsed, mobileMenuOpen, setMobileMenuOpen } = useStore();
const isMobile = useIsMobile();
const { screenSize, isMobileOrTablet, isTablet } = useScreenSize();
const [showBapModal, setShowBapModal] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedRecipient, setSelectedRecipient] = useState<string>('');
const [isUploading, setIsUploading] = useState(false);
const [showWaitingModal, setShowWaitingModal] = useState(false);
const [processingSeconds, setProcessingSeconds] = useState(0);
const [processingTimeout, setProcessingTimeout] = useState<NodeJS.Timeout | null>(null);
const { toast } = useToast();
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file && file.type === 'application/pdf') {
setSelectedFile(file);
} else {
toast({
title: "Erreur",
description: "Veuillez sélectionner un fichier PDF",
variant: "destructive",
});
}
};
const startProcessingTimer = () => {
setProcessingSeconds(0);
const interval = setInterval(() => {
setProcessingSeconds(prev => {
if (prev >= 60) {
clearInterval(interval);
return 60;
}
return prev + 1;
});
}, 1000);
setProcessingTimeout(interval);
};
const handleCloseWaitingModal = () => {
setShowWaitingModal(false);
setProcessingSeconds(0);
if (processingTimeout) {
clearTimeout(processingTimeout);
setProcessingTimeout(null);
}
};
const handleSendBap = async () => {
if (!selectedFile || !selectedRecipient) {
toast({
title: "Erreur",
description: "Veuillez sélectionner un fichier PDF et un destinataire",
variant: "destructive",
});
return;
}
// Récupérer l'URL webhook configurée
let webhookUrl = '';
try {
const configResponse = await fetch('/api/webhook-bap-config', {
credentials: 'include'
});
if (configResponse.ok) {
const config = await configResponse.json();
webhookUrl = config?.webhookUrl;
}
} catch (error) {
console.warn('Impossible de récupérer la config webhook, utilisation URL par défaut');
}
// Utiliser une URL par défaut si pas de configuration
if (!webhookUrl) {
webhookUrl = "https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d";
}
// Fermer le modal de sélection et ouvrir le modal d'attente
setShowBapModal(false);
setShowWaitingModal(true);
setIsUploading(true);
startProcessingTimer();
try {
// Utiliser FormData comme le système de rapprochement qui fonctionne
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('recipient', selectedRecipient);
formData.append('type', 'BAP');
formData.append('fileName', selectedFile.name);
// Créer un AbortController pour gérer le timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes
// Envoyer directement au webhook (comme le rapprochement)
const response = await fetch(webhookUrl, {
method: 'POST',
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Erreur ${response.status}: ${response.statusText}`);
}
handleCloseWaitingModal();
toast({
title: "Succès",
description: "Fichier BAP envoyé avec succès",
});
// Reset des données
setSelectedFile(null);
setSelectedRecipient('');
} catch (error: any) {
handleCloseWaitingModal();
let errorMessage = "Impossible d'envoyer le fichier BAP";
if (error.name === 'AbortError') {
errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)";
} else if (error.message) {
errorMessage = error.message;
}
toast({
title: "Erreur",
description: errorMessage,
variant: "destructive",
});
// Réouvrir le modal de sélection en cas d'erreur
setShowBapModal(true);
} finally {
setIsUploading(false);
}
};
const handleLogout = async () => {
try {
// Force logout via fetch to ensure session is destroyed
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
});
} catch (error) {
console.error('Logout error:', error);
} finally {
// Force redirect to auth page regardless of API response
window.location.href = "/auth";
}
};
const toggleSidebar = () => {
const newCollapsed = !sidebarCollapsed;
setSidebarCollapsed(newCollapsed);
try {
localStorage.setItem('sidebarCollapsed', JSON.stringify(newCollapsed));
} catch {
// ignore
}
};
const getInitials = (firstName?: string, lastName?: string) => {
if (!firstName && !lastName) return "U";
return `${firstName?.[0] || ""}${lastName?.[0] || ""}`.toUpperCase();
};
const isActive = (path: string) => {
if (path === "/" && location === "/") return true;
// L'accueil « / » affiche le tableau de bord (même logique que le menu mobile)
if (path === "/dashboard" && (location === "/" || location === "/dashboard")) return true;
if (path !== "/" && location.startsWith(path)) return true;
return false;
};
const menuSections = [
{
title: "GESTION PRINCIPALE",
items: [
{
path: "/dashboard",
label: "Tableau de bord",
icon: BarChart3,
roles: ["admin", "directeur", "manager", "employee"]
},
{
path: "/calendar",
label: "Calendrier",
icon: Calendar,
roles: ["admin", "directeur", "manager", "employee"]
},
]
},
{
title: "LOGISTIQUE",
items: [
{
path: "/orders",
label: "Commandes",
icon: Package,
roles: ["admin", "directeur", "manager"]
},
{
path: "/deliveries",
label: "Livraisons",
icon: Truck,
roles: ["admin", "directeur", "manager"]
},
{
path: "/bl-reconciliation",
label: "Rapprochement",
icon: FileText,
roles: ["admin", "directeur"]
},
{
path: "/payment-schedule",
label: "Échéance",
icon: CreditCard,
roles: ["admin", "directeur"]
},
{
path: "/avoirs",
label: "Avoirs",
icon: Receipt,
roles: ["admin", "directeur", "manager"]
},
]
},
{
title: "ANALYSES",
items: [
{
path: "/analytics",
label: "Statistiques",
icon: BarChart3,
roles: ["admin", "directeur", "manager"]
},
]
},
{
title: "OPÉRATIONS",
items: [
{
path: "/publicities",
label: "Publicités",
icon: Megaphone,
roles: ["admin", "directeur", "manager", "employee"]
},
{
path: "/customer-orders",
label: "Commandes Client",
icon: ShoppingCart,
roles: ["admin", "directeur", "manager", "employee"]
},
{
path: "/dlc",
label: "Gestion DLC",
icon: Clock,
roles: ["admin", "directeur", "manager", "employee"]
},
{
path: "/tasks",
label: "Tâches",
icon: ListTodo,
roles: ["admin", "directeur", "manager", "employee"]
},
{
path: "/sav",
label: "SAV",
icon: Wrench,
roles: ["admin", "directeur", "manager", "employee"]
},
{
path: "/contacts",
label: "Contacts",
icon: BookUser,
roles: ["admin", "directeur", "manager", "employee"]
},
]
},
];
const adminItems = [
{
path: "/users",
label: "Utilisateurs",
icon: UserCog,
roles: ["admin"]
},
{
path: "/suppliers",
label: "Fournisseurs",
icon: Building,
roles: ["admin"]
},
{
path: "/groups",
label: "Magasins",
icon: Users,
roles: ["admin"]
},
{
path: "/utilities",
label: "Utilitaires",
icon: Settings,
roles: ["admin"]
},
{
path: "#bap",
label: "BAP",
icon: FileUp,
roles: ["admin"],
isButton: true
},
];
const hasPermission = (roles: string[]) => {
return user?.role && roles.includes(user.role);
};
// Classes responsives adaptées avec support tablette
const getSidebarClasses = () => {
if (isMobile) {
return `fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-gray-200 shadow-lg transform transition-transform duration-300 ease-in-out ${mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
}`;
}
// Mode tablette : semi-collapsed par défaut ou collapsed si forcé
if (isTablet) {
return `${sidebarCollapsed ? 'sidebar-collapsed' : 'sidebar-tablet'} bg-white border-r border-gray-200 flex flex-col shadow-lg transition-all duration-300 ease-in-out`;
}
// Mode desktop standard
return `${sidebarCollapsed ? 'sidebar-collapsed' : 'sidebar-expanded'} bg-white border-r border-gray-200 flex flex-col shadow-lg transition-all duration-300 ease-in-out`;
};
const sidebarClasses = getSidebarClasses();
// Si l'utilisateur n'est pas encore chargé, afficher un état de chargement
if (isLoading) {
return (
<aside className={sidebarClasses}>
<div className="h-16 flex items-center justify-between border-b border-gray-200 bg-white px-3">
{/* Mobile close button */}
{isMobile ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-3">
<Store className="h-6 w-6 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">LogiFlow</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setMobileMenuOpen(false)}
className="h-8 w-8 p-0 hover:bg-gray-100"
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<>
{!sidebarCollapsed && (
<div className="flex items-center space-x-3">
<Store className="h-6 w-6 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">LogiFlow</span>
</div>
)}
{sidebarCollapsed && (
<div className="flex items-center justify-center w-full">
<Store className="h-6 w-6 text-blue-600" />
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={toggleSidebar}
className="h-8 w-8 p-0 hover:bg-gray-100"
>
{sidebarCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</>
)}
</div>
<div className="flex-1 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</aside>
);
}
// Si l'utilisateur n'est pas authentifié, afficher seulement le logo
if (!user) {
return (
<aside className={sidebarClasses}>
<div className="h-16 flex items-center justify-between border-b border-gray-200 bg-white px-3">
{/* Mobile close button */}
{isMobile ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-3">
<Store className="h-6 w-6 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">LogiFlow</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setMobileMenuOpen(false)}
className="h-8 w-8 p-0 hover:bg-gray-100"
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<>
{!sidebarCollapsed && (
<div className="flex items-center space-x-3">
<Store className="h-6 w-6 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">LogiFlow</span>
</div>
)}
{sidebarCollapsed && (
<div className="flex items-center justify-center w-full">
<Store className="h-6 w-6 text-blue-600" />
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={toggleSidebar}
className="h-8 w-8 p-0 hover:bg-gray-100"
>
{sidebarCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</>
)}
</div>
<div className="flex-1 flex items-center justify-center">
<div className="text-center text-gray-500">
{!sidebarCollapsed && <p>Authentification requise</p>}
</div>
</div>
</aside>
);
}
const handleMenuClick = () => {
if (isMobile) {
setMobileMenuOpen(false);
}
};
return (
<aside className={sidebarClasses}>
{/* Logo et bouton de collapse */}
<div className="h-16 flex items-center justify-between border-b border-gray-200 bg-white px-3">
{/* Mobile close button */}
{isMobile ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-3">
<Store className="h-6 w-6 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">LogiFlow</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setMobileMenuOpen(false)}
className="h-8 w-8 p-0 hover:bg-gray-100"
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<>
{!sidebarCollapsed && (
<div className="flex items-center space-x-3">
<Store className="h-6 w-6 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">LogiFlow</span>
</div>
)}
{sidebarCollapsed && (
<div className="flex items-center justify-center w-full">
<Store className="h-6 w-6 text-blue-600" />
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={toggleSidebar}
className="h-8 w-8 p-0 hover:bg-gray-100"
>
{sidebarCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</>
)}
</div>
{/* Navigation */}
<nav className="flex-1 py-2 px-3 overflow-y-auto">
<div className="space-y-3">
{menuSections.map((section, sectionIndex) => {
// Vérifier si au moins un item de la section est accessible
const hasAccessibleItems = section.items.some(item => hasPermission(item.roles));
if (!hasAccessibleItems) return null;
return (
<div key={section.title} className="space-y-1">
{/* Titre de section - caché en mode collapsed */}
{!sidebarCollapsed && (
<h3 className="px-3 mb-1 text-[11px] font-bold text-gray-500 uppercase tracking-widest">
{section.title}
</h3>
)}
{/* Séparateur visuel en mode collapsed */}
{sidebarCollapsed && sectionIndex > 0 && (
<div className="border-t border-gray-200 my-2"></div>
)}
{/* Items du menu */}
<div className="space-y-0.5">
{section.items.map((item) => {
const hasRolePermission = hasPermission(item.roles);
if (!hasRolePermission) return null;
const Icon = item.icon;
const active = isActive(item.path);
return (
<Link key={item.path} href={item.path}>
<div
className={`flex items-center transition-colors hover:bg-gray-100 rounded-md ${
// Espacement adaptatif selon l'écran et l'état
sidebarCollapsed && !isMobile
? 'px-3 py-2 justify-center min-h-9'
: isMobileOrTablet
? 'px-4 py-2.5 min-h-10'
: 'px-3 py-2 min-h-9'
} ${active
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600'
: 'text-gray-700'
}`}
title={sidebarCollapsed && !isMobile ? item.label : undefined}
onClick={handleMenuClick}
>
<Icon className={`${
// Tailles d'icônes adaptatives
isMobile ? 'h-5 w-5' :
isTablet ? 'h-6 w-6' :
'h-5 w-5'
} ${active ? 'text-blue-700' : 'text-gray-600'
} ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
{(!sidebarCollapsed || isMobile) && (
<span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}>
{item.label}
</span>
)}
</div>
</Link>
);
})}
</div>
</div>
);
})}
</div>
</nav>
{/* Administration Section */}
{adminItems.some(item => hasPermission(item.roles)) && (
<div className="border-t border-gray-200 py-2 px-3">
{!sidebarCollapsed && (
<div className="mb-1">
<h3 className="px-3 text-[11px] font-bold text-gray-500 uppercase tracking-widest">
Administration
</h3>
</div>
)}
<div className="space-y-0.5">
{adminItems.map((item) => {
if (!hasPermission(item.roles)) return null;
const Icon = item.icon;
const active = isActive(item.path);
if (item.isButton && item.path === '#bap') {
return (
<div key={item.path}>
<div
className={`flex items-center cursor-pointer transition-colors hover:bg-gray-100 rounded-md ${sidebarCollapsed && !isMobile
? 'px-3 py-2 justify-center min-h-9'
: isMobileOrTablet
? 'px-4 py-2.5 min-h-10'
: 'px-3 py-2 min-h-9'
} text-gray-700`}
title={sidebarCollapsed && !isMobile ? item.label : undefined}
onClick={() => {
setShowBapModal(true);
if (isMobile) setMobileMenuOpen(false);
}}
>
<Icon className={`${isMobile ? 'h-5 w-5' :
isTablet ? 'h-6 w-6' :
'h-5 w-5'
} text-gray-600 ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
{(!sidebarCollapsed || isMobile) && (
<span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}>
{item.label}
</span>
)}
</div>
</div>
);
}
return (
<Link key={item.path} href={item.path}>
<div
className={`flex items-center transition-colors hover:bg-gray-100 rounded-md ${sidebarCollapsed && !isMobile
? 'px-3 py-2 justify-center min-h-9'
: isMobileOrTablet
? 'px-4 py-2.5 min-h-10'
: 'px-3 py-2 min-h-9'
} ${active
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600'
: 'text-gray-700'
}`}
title={sidebarCollapsed && !isMobile ? item.label : undefined}
onClick={handleMenuClick}
>
<Icon className={`${isMobile ? 'h-5 w-5' :
isTablet ? 'h-6 w-6' :
'h-5 w-5'
} ${active ? 'text-blue-700' : 'text-gray-600'
} ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
{(!sidebarCollapsed || isMobile) && (
<span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}>
{item.label}
</span>
)}
</div>
</Link>
);
})}
</div>
</div>
)}
{/* Modal BAP */}
<Dialog open={showBapModal} onOpenChange={setShowBapModal}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileUp className="h-5 w-5 text-blue-600" />
Envoi BAP
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* NOUVELLE zone de sélection de fichier */}
<div className="space-y-3">
<Label className="text-sm font-semibold text-gray-800">Fichier PDF</Label>
{!selectedFile ? (
<div
className="border-2 border-dashed border-blue-300 rounded-xl p-8 bg-gradient-to-br from-blue-50 to-indigo-50 hover:from-blue-100 hover:to-indigo-100 transition-all cursor-pointer group"
onClick={() => document.getElementById('hidden-file-input')?.click()}
>
<div className="text-center space-y-4">
<div className="mx-auto h-16 w-16 bg-blue-500 rounded-full flex items-center justify-center group-hover:bg-blue-600 transition-colors">
<FileUp className="h-8 w-8 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-2">
Cliquez ici pour choisir un fichier PDF
</h3>
<p className="text-sm text-gray-600 mb-4">
Ou glissez-déposez votre fichier dans cette zone
</p>
<div className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors">
<FileUp className="h-5 w-5 mr-2" />
Parcourir les fichiers
</div>
</div>
</div>
</div>
) : (
<div className="border-2 border-green-300 rounded-xl p-6 bg-gradient-to-br from-green-50 to-emerald-50">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 bg-green-500 rounded-full flex items-center justify-center">
<FileUp className="h-6 w-6 text-white" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-green-800">{selectedFile.name}</h4>
<p className="text-sm text-green-600">
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB • PDF
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedFile(null);
const input = document.getElementById('hidden-file-input') as HTMLInputElement;
if (input) input.value = '';
}}
className="border-green-300 text-green-700 hover:bg-green-100"
>
Changer
</Button>
</div>
</div>
)}
{/* Input caché */}
<input
id="hidden-file-input"
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
/>
</div>
{/* Sélection destinataire améliorée */}
<div className="space-y-3">
<Label className="text-sm font-semibold text-gray-800">Envoyer à</Label>
<Select value={selectedRecipient} onValueChange={setSelectedRecipient}>
<SelectTrigger className="h-12 border-2 border-gray-200 rounded-lg hover:border-blue-300 transition-colors">
<SelectValue placeholder="🎯 Choisir le destinataire" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Laurie" className="py-3">
<div className="flex items-center space-x-3">
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-purple-600">L</span>
</div>
<span className="font-medium">Laurie</span>
</div>
</SelectItem>
<SelectItem value="Jeremy" className="py-3">
<div className="flex items-center space-x-3">
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-blue-600">J</span>
</div>
<span className="font-medium">Jeremy</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Boutons améliorés */}
<div className="flex gap-3 pt-4 border-t border-gray-200">
<Button
variant="outline"
onClick={() => setShowBapModal(false)}
className="flex-1 h-12 border-2 border-gray-200 hover:border-gray-300"
>
Annuler
</Button>
<Button
onClick={handleSendBap}
disabled={!selectedFile || !selectedRecipient || isUploading}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white font-medium"
>
{isUploading ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full mr-2"></div>
Envoi en cours...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Envoyer le BAP
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Modal d'attente */}
<Dialog open={showWaitingModal} onOpenChange={() => { }}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Traitement en cours...
</DialogTitle>
</DialogHeader>
<div className="space-y-4 text-center">
<div className="flex flex-col items-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div className="text-sm text-gray-600">
Envoi du fichier vers le workflow...
</div>
<div className="text-xs text-gray-500">
{processingSeconds < 60
? `${processingSeconds}s écoulées`
: '60s+ écoulées'
}
</div>
</div>
</div>
</DialogContent>
</Dialog>
{/* User Profile & Logout */}
<div className="border-t border-gray-200 p-4">
{!sidebarCollapsed ? (
<>
<div className="flex items-center space-x-3 mb-3">
<div className="h-8 w-8 bg-gray-100 flex items-center justify-center">
<span className="text-xs font-medium text-gray-700">
{getInitials(user?.firstName, user?.lastName)}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user?.firstName || user?.lastName ? `${user?.firstName || ''} ${user?.lastName || ''}`.trim() : user?.username}
</p>
<p className="text-xs text-gray-500 truncate">
{user?.role === 'admin' ? 'Administrateur' :
user?.role === 'manager' ? 'Manager' :
user?.role === 'directeur' ? 'Directeur' : 'Employé'}
</p>
</div>
</div>
<Button
onClick={handleLogout}
variant="ghost"
size="sm"
className="w-full justify-start text-gray-700 hover:text-gray-900 hover:bg-gray-100"
>
<LogOut className="mr-2 h-4 w-4" />
Déconnexion
</Button>
</>
) : (
<div className="flex flex-col items-center space-y-3">
<div className="h-8 w-8 bg-gray-100 flex items-center justify-center">
<span className="text-xs font-medium text-gray-700">
{getInitials(user?.firstName, user?.lastName)}
</span>
</div>
<Button
onClick={handleLogout}
variant="ghost"
size="sm"
className="w-8 h-8 p-0 text-gray-700 hover:text-gray-900 hover:bg-gray-100"
title="Déconnexion"
>
<LogOut className="h-4 w-4" />
</Button>
</div>
)}
</div>
</aside>
);
}