feat: implement Sidebar component including navigation, user details, logout, and BAP document upload with webhook integration.

This commit is contained in:
Michael committed 2025-12-08 09:12:45 +01:00
1 parent 9ec9cfcca3
commit 6b719fb111
1 file changed
+176 -185
+176 -185
View File
@@ -11,15 +11,15 @@ import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import { import {
Store, Store,
Calendar, Calendar,
BarChart3, BarChart3,
Package, Package,
Truck, Truck,
Building, Building,
Users, Users,
UserCog, UserCog,
LogOut, LogOut,
FileText, FileText,
Megaphone, Megaphone,
@@ -106,7 +106,7 @@ export default function Sidebar() {
const configResponse = await fetch('/api/webhook-bap-config', { const configResponse = await fetch('/api/webhook-bap-config', {
credentials: 'include' credentials: 'include'
}); });
if (configResponse.ok) { if (configResponse.ok) {
const config = await configResponse.json(); const config = await configResponse.json();
webhookUrl = config?.webhookUrl; webhookUrl = config?.webhookUrl;
@@ -150,9 +150,9 @@ export default function Sidebar() {
if (!response.ok) { if (!response.ok) {
throw new Error(`Erreur ${response.status}: ${response.statusText}`); throw new Error(`Erreur ${response.status}: ${response.statusText}`);
} }
handleCloseWaitingModal(); handleCloseWaitingModal();
toast({ toast({
title: "Succès", title: "Succès",
description: "Fichier BAP envoyé avec succès", description: "Fichier BAP envoyé avec succès",
@@ -161,23 +161,23 @@ export default function Sidebar() {
// Reset des données // Reset des données
setSelectedFile(null); setSelectedFile(null);
setSelectedRecipient(''); setSelectedRecipient('');
} catch (error: any) { } catch (error: any) {
handleCloseWaitingModal(); handleCloseWaitingModal();
let errorMessage = "Impossible d'envoyer le fichier BAP"; let errorMessage = "Impossible d'envoyer le fichier BAP";
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)"; errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)";
} else if (error.message) { } else if (error.message) {
errorMessage = error.message; errorMessage = error.message;
} }
toast({ toast({
title: "Erreur", title: "Erreur",
description: errorMessage, description: errorMessage,
variant: "destructive", variant: "destructive",
}); });
// Réouvrir le modal de sélection en cas d'erreur // Réouvrir le modal de sélection en cas d'erreur
setShowBapModal(true); setShowBapModal(true);
} finally { } finally {
@@ -193,7 +193,7 @@ export default function Sidebar() {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
// Force logout via fetch to ensure session is destroyed // Force logout via fetch to ensure session is destroyed
await fetch('/api/logout', { await fetch('/api/logout', {
method: 'POST', method: 'POST',
credentials: 'include' credentials: 'include'
}); });
@@ -232,138 +232,138 @@ export default function Sidebar() {
{ {
title: "GESTION PRINCIPALE", title: "GESTION PRINCIPALE",
items: [ items: [
{ {
path: "/dashboard", path: "/dashboard",
label: "Tableau de bord", label: "Tableau de bord",
icon: BarChart3, icon: BarChart3,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
{ {
path: "/calendar", path: "/calendar",
label: "Calendrier", label: "Calendrier",
icon: Calendar, icon: Calendar,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
] ]
}, },
{ {
title: "LOGISTIQUE", title: "LOGISTIQUE",
items: [ items: [
{ {
path: "/orders", path: "/orders",
label: "Commandes", label: "Commandes",
icon: Package, icon: Package,
roles: ["admin", "directeur", "manager"] roles: ["admin", "directeur", "manager"]
}, },
{ {
path: "/deliveries", path: "/deliveries",
label: "Livraisons", label: "Livraisons",
icon: Truck, icon: Truck,
roles: ["admin", "directeur", "manager"] roles: ["admin", "directeur", "manager"]
}, },
{ {
path: "/bl-reconciliation", path: "/bl-reconciliation",
label: "Rapprochement", label: "Rapprochement",
icon: FileText, icon: FileText,
roles: ["admin", "directeur"] roles: ["admin", "directeur"]
}, },
{ {
path: "/payment-schedule", path: "/payment-schedule",
label: "Échéance", label: "Échéance",
icon: CreditCard, icon: CreditCard,
roles: ["admin", "directeur"] roles: ["admin", "directeur"]
}, },
{ {
path: "/avoirs", path: "/avoirs",
label: "Avoirs", label: "Avoirs",
icon: Receipt, icon: Receipt,
roles: ["admin", "directeur", "manager"] roles: ["admin", "directeur", "manager"]
}, },
] ]
}, },
{ {
title: "ANALYSES", title: "ANALYSES",
items: [ items: [
{ {
path: "/analytics", path: "/analytics",
label: "Statistiques", label: "Statistiques",
icon: BarChart3, icon: BarChart3,
roles: ["admin", "directeur", "manager"] roles: ["admin", "directeur", "manager"]
}, },
{ {
path: "/sales-analysis", path: "/sales-analysis",
label: "Analyse Vente", label: "Analyse Vente",
icon: TrendingUp, icon: TrendingUp,
roles: ["admin", "directeur"] roles: ["admin", "directeur"]
}, },
] ]
}, },
{ {
title: "OPÉRATIONS", title: "OPÉRATIONS",
items: [ items: [
{ {
path: "/publicities", path: "/publicities",
label: "Publicités", label: "Publicités",
icon: Megaphone, icon: Megaphone,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
{ {
path: "/customer-orders", path: "/customer-orders",
label: "Commandes Client", label: "Commandes Client",
icon: ShoppingCart, icon: ShoppingCart,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
{ {
path: "/dlc", path: "/dlc",
label: "Gestion DLC", label: "Gestion DLC",
icon: Clock, icon: Clock,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
{ {
path: "/tasks", path: "/tasks",
label: "Tâches", label: "Tâches",
icon: ListTodo, icon: ListTodo,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
{ {
path: "/sav", path: "/sav",
label: "SAV", label: "SAV",
icon: Wrench, icon: Wrench,
roles: ["admin", "directeur", "manager", "employee"] roles: ["admin", "directeur", "manager", "employee"]
}, },
] ]
}, },
]; ];
const adminItems = [ const adminItems = [
{ {
path: "/users", path: "/users",
label: "Utilisateurs", label: "Utilisateurs",
icon: UserCog, icon: UserCog,
roles: ["admin"] roles: ["admin"]
}, },
{ {
path: "/suppliers", path: "/suppliers",
label: "Fournisseurs", label: "Fournisseurs",
icon: Building, icon: Building,
roles: ["admin"] roles: ["admin"]
}, },
{ {
path: "/groups", path: "/groups",
label: "Magasins", label: "Magasins",
icon: Users, icon: Users,
roles: ["admin"] roles: ["admin"]
}, },
{ {
path: "/utilities", path: "/utilities",
label: "Utilitaires", label: "Utilitaires",
icon: Settings, icon: Settings,
roles: ["admin"] roles: ["admin"]
}, },
{ {
path: "#bap", path: "#bap",
label: "BAP", label: "BAP",
icon: FileUp, icon: FileUp,
roles: ["admin"], roles: ["admin"],
isButton: true isButton: true
}, },
@@ -383,27 +383,26 @@ export default function Sidebar() {
// Classes responsives adaptées avec support tablette // Classes responsives adaptées avec support tablette
const getSidebarClasses = () => { const getSidebarClasses = () => {
if (isMobile) { 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 ${ 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'
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full' }`;
}`;
} }
// Mode tablette : semi-collapsed par défaut ou collapsed si forcé // Mode tablette : semi-collapsed par défaut ou collapsed si forcé
if (isTablet) { 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`; 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 // 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`; 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(); const sidebarClasses = getSidebarClasses();
// Debug log pour vérifier l'état de la sidebar // Debug log pour vérifier l'état de la sidebar
console.log('🔧 Sidebar Debug:', { console.log('🔧 Sidebar Debug:', {
isMobile, isMobile,
sidebarCollapsed, sidebarCollapsed,
mobileMenuOpen, mobileMenuOpen,
width: sidebarCollapsed ? 'collapsed (64px)' : 'expanded (256px)', width: sidebarCollapsed ? 'collapsed (64px)' : 'expanded (256px)',
classes: sidebarClasses classes: sidebarClasses
}); });
@@ -578,7 +577,7 @@ export default function Sidebar() {
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 py-2 px-3 overflow-y-hidden"> <nav className="flex-1 py-2 px-3 overflow-y-auto">
<div className="space-y-3"> <div className="space-y-3">
{menuSections.map((section, sectionIndex) => { {menuSections.map((section, sectionIndex) => {
// Vérifier si au moins un item de la section est accessible // Vérifier si au moins un item de la section est accessible
@@ -593,48 +592,46 @@ export default function Sidebar() {
{section.title} {section.title}
</h3> </h3>
)} )}
{/* Séparateur visuel en mode collapsed */} {/* Séparateur visuel en mode collapsed */}
{sidebarCollapsed && sectionIndex > 0 && ( {sidebarCollapsed && sectionIndex > 0 && (
<div className="border-t border-gray-200 my-2"></div> <div className="border-t border-gray-200 my-2"></div>
)} )}
{/* Items du menu */} {/* Items du menu */}
<div className="space-y-0.5"> <div className="space-y-0.5">
{section.items.map((item) => { {section.items.map((item) => {
const hasRolePermission = hasPermission(item.roles); const hasRolePermission = hasPermission(item.roles);
if (!hasRolePermission) return null; if (!hasRolePermission) return null;
const Icon = item.icon; const Icon = item.icon;
const active = isActive(item.path); const active = isActive(item.path);
return ( return (
<Link key={item.path} href={item.path}> <Link key={item.path} href={item.path}>
<div <div
className={`flex items-center transition-colors hover:bg-gray-100 rounded-md ${ className={`flex items-center transition-colors hover:bg-gray-100 rounded-md ${
// Espacement adaptatif selon l'écran et l'état // Espacement adaptatif selon l'écran et l'état
sidebarCollapsed && !isMobile sidebarCollapsed && !isMobile
? 'px-3 py-2 justify-center min-h-9' ? 'px-3 py-2 justify-center min-h-9'
: isMobileOrTablet : isMobileOrTablet
? 'px-4 py-2.5 min-h-10' ? 'px-4 py-2.5 min-h-10'
: 'px-3 py-2 min-h-9' : 'px-3 py-2 min-h-9'
} ${ } ${active
active
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600'
: 'text-gray-700' : 'text-gray-700'
}`} }`}
title={sidebarCollapsed && !isMobile ? item.label : undefined} title={sidebarCollapsed && !isMobile ? item.label : undefined}
onClick={handleMenuClick} onClick={handleMenuClick}
> >
<Icon className={`${ <Icon className={`${
// Tailles d'icônes adaptatives // Tailles d'icônes adaptatives
isMobile ? 'h-5 w-5' : isMobile ? 'h-5 w-5' :
isTablet ? 'h-6 w-6' : isTablet ? 'h-6 w-6' :
'h-5 w-5' 'h-5 w-5'
} ${ } ${active ? 'text-blue-700' : 'text-gray-600'
active ? 'text-blue-700' : 'text-gray-600' } ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
} ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
{(!sidebarCollapsed || isMobile) && ( {(!sidebarCollapsed || isMobile) && (
<span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}> <span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}>
{item.label} {item.label}
@@ -664,32 +661,30 @@ export default function Sidebar() {
<div className="space-y-0.5"> <div className="space-y-0.5">
{adminItems.map((item) => { {adminItems.map((item) => {
if (!hasPermission(item.roles)) return null; if (!hasPermission(item.roles)) return null;
const Icon = item.icon; const Icon = item.icon;
const active = isActive(item.path); const active = isActive(item.path);
if (item.isButton && item.path === '#bap') { if (item.isButton && item.path === '#bap') {
return ( return (
<div key={item.path}> <div key={item.path}>
<div <div
className={`flex items-center cursor-pointer transition-colors hover:bg-gray-100 rounded-md ${ className={`flex items-center cursor-pointer transition-colors hover:bg-gray-100 rounded-md ${sidebarCollapsed && !isMobile
sidebarCollapsed && !isMobile ? 'px-3 py-2 justify-center min-h-9'
? 'px-3 py-2 justify-center min-h-9' : isMobileOrTablet
: isMobileOrTablet ? 'px-4 py-2.5 min-h-10'
? 'px-4 py-2.5 min-h-10'
: 'px-3 py-2 min-h-9' : 'px-3 py-2 min-h-9'
} text-gray-700`} } text-gray-700`}
title={sidebarCollapsed && !isMobile ? item.label : undefined} title={sidebarCollapsed && !isMobile ? item.label : undefined}
onClick={() => { onClick={() => {
setShowBapModal(true); setShowBapModal(true);
if (isMobile) setMobileMenuOpen(false); if (isMobile) setMobileMenuOpen(false);
}} }}
> >
<Icon className={`${ <Icon className={`${isMobile ? 'h-5 w-5' :
isMobile ? 'h-5 w-5' : isTablet ? 'h-6 w-6' :
isTablet ? 'h-6 w-6' : 'h-5 w-5'
'h-5 w-5' } text-gray-600 ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
} text-gray-600 ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
{(!sidebarCollapsed || isMobile) && ( {(!sidebarCollapsed || isMobile) && (
<span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}> <span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}>
{item.label} {item.label}
@@ -699,31 +694,27 @@ export default function Sidebar() {
</div> </div>
); );
} }
return ( return (
<Link key={item.path} href={item.path}> <Link key={item.path} href={item.path}>
<div <div
className={`flex items-center transition-colors hover:bg-gray-100 rounded-md ${ className={`flex items-center transition-colors hover:bg-gray-100 rounded-md ${sidebarCollapsed && !isMobile
sidebarCollapsed && !isMobile ? 'px-3 py-2 justify-center min-h-9'
? 'px-3 py-2 justify-center min-h-9' : isMobileOrTablet
: isMobileOrTablet ? 'px-4 py-2.5 min-h-10'
? 'px-4 py-2.5 min-h-10'
: 'px-3 py-2 min-h-9' : 'px-3 py-2 min-h-9'
} ${ } ${active
active
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600'
: 'text-gray-700' : 'text-gray-700'
}`} }`}
title={sidebarCollapsed && !isMobile ? item.label : undefined} title={sidebarCollapsed && !isMobile ? item.label : undefined}
onClick={handleMenuClick} onClick={handleMenuClick}
> >
<Icon className={`${ <Icon className={`${isMobile ? 'h-5 w-5' :
isMobile ? 'h-5 w-5' : isTablet ? 'h-6 w-6' :
isTablet ? 'h-6 w-6' : 'h-5 w-5'
'h-5 w-5' } ${active ? 'text-blue-700' : 'text-gray-600'
} ${ } ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
active ? 'text-blue-700' : 'text-gray-600'
} ${sidebarCollapsed && !isMobile ? '' : 'mr-3'}`} />
{(!sidebarCollapsed || isMobile) && ( {(!sidebarCollapsed || isMobile) && (
<span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}> <span className={`${isMobileOrTablet ? 'text-base' : 'text-[15px]'} font-medium`}>
{item.label} {item.label}
@@ -750,9 +741,9 @@ export default function Sidebar() {
{/* NOUVELLE zone de sélection de fichier */} {/* NOUVELLE zone de sélection de fichier */}
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-sm font-semibold text-gray-800">Fichier PDF</Label> <Label className="text-sm font-semibold text-gray-800">Fichier PDF</Label>
{!selectedFile ? ( {!selectedFile ? (
<div <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" 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()} onClick={() => document.getElementById('hidden-file-input')?.click()}
> >
@@ -786,8 +777,8 @@ export default function Sidebar() {
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB • PDF {(selectedFile.size / 1024 / 1024).toFixed(2)} MB • PDF
</p> </p>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { onClick={() => {
setSelectedFile(null); setSelectedFile(null);
@@ -801,7 +792,7 @@ export default function Sidebar() {
</div> </div>
</div> </div>
)} )}
{/* Input caché */} {/* Input caché */}
<input <input
id="hidden-file-input" id="hidden-file-input"
@@ -840,14 +831,14 @@ export default function Sidebar() {
</div> </div>
{/* Boutons améliorés */} {/* Boutons améliorés */}
<div className="flex gap-3 pt-4 border-t border-gray-200"> <div className="flex gap-3 pt-4 border-t border-gray-200">
<Button <Button
variant="outline" variant="outline"
onClick={() => setShowBapModal(false)} onClick={() => setShowBapModal(false)}
className="flex-1 h-12 border-2 border-gray-200 hover:border-gray-300" className="flex-1 h-12 border-2 border-gray-200 hover:border-gray-300"
> >
Annuler Annuler
</Button> </Button>
<Button <Button
onClick={handleSendBap} onClick={handleSendBap}
disabled={!selectedFile || !selectedRecipient || isUploading} disabled={!selectedFile || !selectedRecipient || isUploading}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white font-medium" className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white font-medium"
@@ -870,7 +861,7 @@ export default function Sidebar() {
</Dialog> </Dialog>
{/* Modal d'attente */} {/* Modal d'attente */}
<Dialog open={showWaitingModal} onOpenChange={() => {}}> <Dialog open={showWaitingModal} onOpenChange={() => { }}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@@ -885,7 +876,7 @@ export default function Sidebar() {
Envoi du fichier vers le workflow... Envoi du fichier vers le workflow...
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{processingSeconds < 60 {processingSeconds < 60
? `${processingSeconds}s écoulées` ? `${processingSeconds}s écoulées`
: '60s+ écoulées' : '60s+ écoulées'
} }
@@ -910,13 +901,13 @@ export default function Sidebar() {
{user?.firstName || user?.lastName ? `${user?.firstName || ''} ${user?.lastName || ''}`.trim() : user?.username} {user?.firstName || user?.lastName ? `${user?.firstName || ''} ${user?.lastName || ''}`.trim() : user?.username}
</p> </p>
<p className="text-xs text-gray-500 truncate"> <p className="text-xs text-gray-500 truncate">
{user?.role === 'admin' ? 'Administrateur' : {user?.role === 'admin' ? 'Administrateur' :
user?.role === 'manager' ? 'Manager' : user?.role === 'manager' ? 'Manager' :
user?.role === 'directeur' ? 'Directeur' : 'Employé'} user?.role === 'directeur' ? 'Directeur' : 'Employé'}
</p> </p>
</div> </div>
</div> </div>
<Button <Button
onClick={handleLogout} onClick={handleLogout}
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -933,7 +924,7 @@ export default function Sidebar() {
{getInitials(user?.firstName, user?.lastName)} {getInitials(user?.firstName, user?.lastName)}
</span> </span>
</div> </div>
<Button <Button
onClick={handleLogout} onClick={handleLogout}
variant="ghost" variant="ghost"
size="sm" size="sm"