docv/app/dashboard/layout.tsx
2025-10-21 11:42:22 +02:00

363 lines
14 KiB
TypeScript

"use client"
import type React from "react"
import { useState, useEffect, useCallback } from "react"
import { useRouter, usePathname } from "next/navigation"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import {
Shield,
FileText,
Folder,
Users,
Settings,
Search,
MessageCircle,
Bell,
User,
LogOut,
Menu,
X,
ChevronDown,
ChevronRight,
Home,
Key,
LayoutDashboard,
TestTube,
} from "@/lib/icons"
import UserStore from "@/lib/4nk/UserStore"
import { iframeUrl } from "../page"
import EventBus from "@/lib/4nk/EventBus"
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const [processes, setProcesses] = useState<any>(null)
const [myProcesses, setMyProcesses] = useState<string[]>([])
const [isConnected, setIsConnected] = useState(false)
const [userPairingId, setUserPairingId] = useState<string | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [isMockMode, setIsMockMode] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(false)
const [userInfo, setUserInfo] = useState<any>(null)
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false)
const [isPrivateKeyFlash, setIsPrivateKeyFlash] = useState(false)
const [currentFolderType, setCurrentFolderType] = useState<string | undefined>(undefined)
const router = useRouter()
const pathname = usePathname()
const navigation: Array<{ name: string; href: string; icon: any; type?: string }> = [
{ name: "My work", href: "/dashboard", icon: LayoutDashboard },
{ name: "Dossier", href: "/dashboard/folders", icon: Folder },
]
// Appliquer le thème global dès le chargement (préférence stockée)
useEffect(() => {
try {
const saved = typeof window !== 'undefined' ? localStorage.getItem('theme') : null
const dark = saved ? saved === 'dark' : (typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches)
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', !!dark)
}
} catch { }
}, [])
useEffect(() => {
const connected = UserStore.getInstance().isConnected();
console.log('[Login] User connected:', connected);
setIsConnected(connected);
}, []);
useEffect(() => {
const pairingId = UserStore.getInstance().getUserPairingId();
console.log('[Login] User pairing ID:', pairingId);
setUserPairingId(pairingId);
}, []);
useEffect(() => {
const checkAuthentication = async () => {
try {
const userStore = UserStore.getInstance()
const accessToken = userStore.getAccessToken()
if (accessToken) {
setIsAuthenticated(true)
const pairingId = userStore.getUserPairingId()
setUserInfo({
id: pairingId?.slice(0, 8) + "...",
name: "Utilisateur 4NK",
email: "user@4nk.io",
role: "Utilisateur",
company: "Organisation 4NK",
})
}
} catch (error) {
console.error("Error checking authentication:", error)
setIsAuthModalOpen(true)
} finally {
setIsLoading(false)
}
}
checkAuthentication()
}, [iframeUrl])
const handleAuthSuccess = () => {
setIsAuthModalOpen(false)
setIsAuthenticated(true)
// Recharger la page pour récupérer les nouvelles données
window.location.reload()
}
const handleLogout = useCallback(() => {
UserStore.getInstance().disconnect();
setIsConnected(false);
setProcesses(null);
setMyProcesses([]);
setUserPairingId(null);
// Émettre un événement pour vider les messages locaux
EventBus.getInstance().emit('CLEAR_CONSOLE');
setShowLogoutConfirm(true)
}, []);
// Stabilise la lecture de query params côté client pour éviter les décalages SSR/CSR
useEffect(() => {
if (typeof window !== "undefined") {
const updateType = () => {
const t = new URLSearchParams(window.location.search).get("type") || undefined
setCurrentFolderType(t || undefined)
}
updateType()
window.addEventListener("popstate", updateType)
return () => window.removeEventListener("popstate", updateType)
}
}, [pathname])
useEffect(() => {
const onPrivateKeyAccess = () => {
setIsPrivateKeyFlash(true)
setTimeout(() => setIsPrivateKeyFlash(false), 400)
}
if (typeof window !== "undefined") {
window.addEventListener("private-key-access", onPrivateKeyAccess as EventListener)
}
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("private-key-access", onPrivateKeyAccess as EventListener)
}
}
}, [])
// Suppression des retours conditionnels précoces pour stabiliser l'ordre des hooks
return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
{/* Sidebar mobile overlay */}
{sidebarOpen && (
<div className="fixed inset-0 z-40 lg:hidden">
<div
className="fixed inset-0 bg-gray-600 bg-opacity-75"
onClick={() => setSidebarOpen(false)}
/>
</div>
)}
{/* Sidebar */}
<div
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-800 shadow-lg transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:relative lg:flex lg:flex-col ${sidebarOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div className="flex flex-col h-full">
{/* Logo */}
<div className="flex items-center justify-between h-16 px-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-2">
<Shield className="h-8 w-8 text-blue-600 dark:text-blue-400" />
<span className="text-xl font-bold text-gray-900 dark:text-gray-100">DocV</span>
{isMockMode && (
<Badge
variant="outline"
className="bg-green-50 dark:bg-green-800 text-green-700 dark:text-green-200 border-green-200 dark:border-green-700 text-xs"
>
<TestTube className="h-3 w-3 mr-1" />
Démo
</Badge>
)}
</div>
<Button variant="ghost" size="sm" className="lg:hidden" onClick={() => setSidebarOpen(false)}>
<X className="h-5 w-5 text-gray-900 dark:text-gray-100" />
</Button>
</div>
{/* User info */}
{userInfo && (
<div className="px-6 py-4 border-b bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
<span className="text-blue-600 dark:text-blue-400 font-medium text-sm">
{userInfo.name.charAt(0)}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{userInfo.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{userInfo.company}</p>
</div>
</div>
</div>
)}
{/* Navigation */}
<nav className="flex-1 px-4 py-4 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const isActive =
pathname === item.href ||
(item.type && pathname.startsWith("/dashboard/folders") && currentFolderType === item.type);
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors ${isActive
? "bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-400"
: "text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
}`}
onClick={() => setSidebarOpen(false)}
>
<item.icon className="h-5 w-5 mr-3" />
{item.name}
{isActive && <ChevronRight className="h-4 w-4 ml-auto" />}
</Link>
);
})}
</nav>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<div className="space-y-2">
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<span>Sécurisé par 4NK</span>
<Shield className="h-3 w-3" />
</div>
{isMockMode && (
<div className="text-xs text-green-600 dark:text-green-300 bg-green-50 dark:bg-green-800 p-2 rounded">
Mode démonstration actif
</div>
)}
<Button
variant="outline"
size="sm"
onClick={handleLogout}
className="w-full bg-transparent dark:bg-transparent"
>
<LogOut className="h-4 w-4 mr-2" />
Déconnexion
</Button>
</div>
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top bar */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="sm" className="lg:hidden" onClick={() => setSidebarOpen(true)}>
<Menu className="h-5 w-5 text-gray-900 dark:text-gray-100" />
</Button>
<div className="hidden lg:block">
<nav className="flex space-x-1 text-sm text-gray-500 dark:text-gray-400">
<Link href="/dashboard" className="hover:text-gray-700 dark:hover:text-gray-100">
My work
</Link>
{pathname !== "/dashboard" && (
<>
<ChevronRight className="h-4 w-4 mx-1 text-gray-500 dark:text-gray-400" />
<span className="text-gray-900 dark:text-gray-100 font-medium">
{(() => {
if (pathname.startsWith("/dashboard/folders") && currentFolderType) {
const match = navigation.find((it) => it.type === currentFolderType);
return match?.name || "Dossiers";
}
return navigation.find((it) => it.href === pathname)?.name || "Page";
})()}
</span>
</>
)}
</nav>
</div>
</div>
<div className="flex items-center space-x-3">
{isMockMode && (
<Badge
variant="outline"
className="bg-green-50 dark:bg-green-800 text-green-700 dark:text-green-200 border-green-200 dark:border-green-700"
>
<TestTube className="h-4 w-4 mr-1" />
Mode Démo
</Badge>
)}
<Button variant="ghost" size="sm">
<Bell className="h-5 w-5 text-gray-900 dark:text-gray-100" />
</Button>
<Link href="/dashboard/settings">
<Button variant="ghost" size="sm" title="Paramètres">
<Settings className="h-5 w-5 text-gray-900 dark:text-gray-100" />
</Button>
</Link>
<div
title="Accès à la clé privée"
className={`h-9 w-9 flex items-center justify-center ${isPrivateKeyFlash ? "text-red-600" : "text-gray-700 dark:text-gray-300"
}`}
aria-hidden
>
<Key className="h-5 w-5" />
</div>
</div>
</div>
</div>
{/* Page content */}
<main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900">
<div className="p-6">{children}</div>
</main>
</div>
{/* Modal de confirmation de déconnexion */}
{showLogoutConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<div className="text-center">
<Shield className="h-12 w-12 mx-auto mb-4 text-blue-600 dark:text-blue-400" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Déconnexion réussie</h3>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Vous avez é déconnecté de votre espace sécurisé DocV.
</p>
<div className="space-y-3">
<Button onClick={() => router.push("/")} variant="outline" className="w-full">
<Home className="h-4 w-4 mr-2" />
Retourner à l'accueil
</Button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4">
Vos données restent sécurisées par le chiffrement 4NK
</p>
</div>
</div>
</div>
)}
</div>
)
}