"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState, useEffect, useRef } from "react"; import { Shield, KeyRound, Home, FileSignature, Banknote, Users, Percent, FolderOpen, Receipt, Building2, UserCog, Edit3, LifeBuoy, CreditCard, Calculator, Euro, Mail, Database, Clapperboard, LogOut } from "lucide-react"; // import { api } from "@/lib/fetcher"; import { createPortal } from "react-dom"; import LogoutButton from "@/components/LogoutButton"; import { useDemoMode } from "@/hooks/useDemoMode"; function AccessLink({ disabled, fullWidth = false }: { disabled: boolean; fullWidth?: boolean }) { const btnRef = useRef(null); const [tipOpen, setTipOpen] = useState(false); const [tipPos, setTipPos] = useState<{ top: number; left: number } | null>(null); function computePos() { const el = btnRef.current; if (!el) return; const r = el.getBoundingClientRect(); setTipPos({ top: r.top + window.scrollY, left: r.left + window.scrollX + r.width / 2 }); } useEffect(() => { if (!tipOpen) return; const onScroll = () => computePos(); const onResize = () => computePos(); window.addEventListener('scroll', onScroll, true); window.addEventListener('resize', onResize); return () => { window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onResize); }; }, [tipOpen]); if (disabled) { return ( <> { computePos(); setTipOpen(true); }} onMouseLeave={() => setTipOpen(false)} onFocus={() => { computePos(); setTipOpen(true); }} onBlur={() => setTipOpen(false)} className={ `inline-flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium bg-emerald-50 text-emerald-700/60 cursor-not-allowed ${fullWidth ? 'w-full justify-center' : ''}` } role="link" aria-disabled="true" tabIndex={0} > Vos accès {tipOpen && tipPos && createPortal( (() => { const top = tipPos.top - 10; // au-dessus const left = tipPos.left; return (
Vous n'avez pas les habilitations nécessaires pour accéder à cette page
); })(), document.body )} ); } return ( Vos accès ); } function DisabledMenuItem({ icon: Icon, label, tooltipMessage }: { icon: any; label: string; tooltipMessage: string }) { const btnRef = useRef(null); const [tipOpen, setTipOpen] = useState(false); const [tipPos, setTipPos] = useState<{ top: number; left: number } | null>(null); function computePos() { const el = btnRef.current; if (!el) return; const r = el.getBoundingClientRect(); // Position à droite de l'élément setTipPos({ top: r.top + window.scrollY + r.height / 2, left: r.right + window.scrollX + 8 // 8px de marge à droite }); } useEffect(() => { if (!tipOpen) return; const onScroll = () => computePos(); const onResize = () => computePos(); window.addEventListener('scroll', onScroll, true); window.addEventListener('resize', onResize); return () => { window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onResize); }; }, [tipOpen]); return ( <> { computePos(); setTipOpen(true); }} onMouseLeave={() => setTipOpen(false)} onFocus={() => { computePos(); setTipOpen(true); }} onBlur={() => setTipOpen(false)} className="block px-3 py-2 rounded-xl text-sm cursor-not-allowed opacity-50" role="link" aria-disabled="true" tabIndex={0} > {label} {tipOpen && tipPos && createPortal( (() => { const top = tipPos.top; const left = tipPos.left; return (
{/* Flèche pointant vers la gauche */}
{tooltipMessage}
); })(), document.body )} ); } type ClientInfo = { id: string; name: string; api_name?: string; user?: { id: string; email: string; display_name?: string; first_name?: string; } | null; } | null; function isActivePath(pathname: string, href: string) { if (href === "/") return pathname === "/"; // dashboard only on exact root // Spécifique : l'onglet Contrats & Paies doit aussi être actif sur /contrats-multi if (href === "/contrats") { if (pathname === "/contrats" || pathname.startsWith("/contrats/")) return true; if (pathname.startsWith("/contrats-multi")) return true; if (pathname.startsWith("/contrats-rg")) return true; return false; } if (href === "/virements-salaires") { if (pathname === "/virements-salaires" || pathname.startsWith("/virements-salaires/")) return true; return false; } return pathname === href || pathname.startsWith(href + "/"); } const items = [ { href: "/", label: "Tableau de bord", icon: Home }, { href: "/contrats", label: "Contrats & Paies", icon: FileSignature }, { href: "/signatures-electroniques", label: "Signatures électroniques", icon: Edit3 }, { href: "/virements-salaires", label: "Virements de salaires", icon: Banknote }, { href: "/salaries", label: "Salariés", icon: Users }, { href: "/cotisations", label: "Cotisations", icon: Percent }, ]; // Hook pour l'horloge en temps réel function useCurrentTime() { // Important: initialiser mais ne pas utiliser avant le montage pour éviter la // divergence SSR/CSR (l'heure évolue entre le rendu serveur et l'hydratation) const [currentTime, setCurrentTime] = useState(new Date()); useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); }, 1000); return () => clearInterval(timer); }, []); return currentTime; } // Composant horloge function LiveClock() { // Ne rien afficher de dépendant du temps pendant l'hydratation const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); const currentTime = useCurrentTime(); const timeString = mounted ? currentTime.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', second: '2-digit', }) : '—:—:—'; const dateString = mounted ? currentTime.toLocaleDateString('fr-FR', { timeZone: 'Europe/Paris', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) : '—'; return (
{timeString}
{dateString}
); } export default function Sidebar({ clientInfo, isStaff = false, mobile = false, onNavigate }: { clientInfo?: ClientInfo, isStaff?: boolean, mobile?: boolean, onNavigate?: () => void }) { const pathname = usePathname(); const [canManageAccess, setCanManageAccess] = useState(false); const [userRole, setUserRole] = useState(null); // 🎭 Détection du mode démo const { isDemoMode } = useDemoMode(); // Signature count in sidebar disabled to reduce Airtable load useEffect(() => { let cancelled = false; async function run() { try { const res = await fetch('/api/me/role', { credentials: 'include', cache: 'no-store' }); if (!res.ok) throw new Error(String(res.status)); const j = await res.json(); const role = (j?.role ? String(j.role).toUpperCase() : null) as string | null; if (!cancelled) { setUserRole(role); setCanManageAccess(role === 'ADMIN' || role === 'SUPER_ADMIN' || j?.isStaff === true); } } catch { if (!cancelled) { setUserRole(null); setCanManageAccess(false); } } } run(); return () => { cancelled = true; }; }, [clientInfo?.id, clientInfo?.user?.id]); // Removed: polling signatures count to avoid Airtable 429s // Plus besoin de useState ou useEffect - les infos viennent directement du serveur const orgName = clientInfo?.name || "Organisation"; const rawFirstName = clientInfo?.user?.first_name || clientInfo?.user?.display_name?.split(' ')[0] || null; const userFirstName = rawFirstName ? rawFirstName.trim().replace(/^./, (ch) => ch.toLocaleUpperCase('fr-FR')) : null; return ( ); }