import { useState, useEffect, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext.jsx'; import { useUi } from '../context/UiContext.jsx'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; import { memberInitials, memberLabel } from '../utils/format.js'; /* ── Icons ───────────────────────────────────────────────────── */ function IconUser() { return ; } function IconLogout() { return ; } function IconAdmin() { return ; } function IconSettings() { return ; } function IconAide() { return ; } function IconComm() { return ; } function IconRefresh() { return ; } function IconChevronRight() { return ; } function IconChevronUp({ open }) { return ( ); } function IconCheck() { return ; } function IconTeam() { return ; } /* ── Avatars ─────────────────────────────────────────────────── */ function UserAvatar({ user, size = 36 }) { const initials = user?.display_name ? user.display_name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase() : (user?.email || '?')[0].toUpperCase(); return (
{initials}
); } function MemberAvatar({ member, size = 28 }) { const isEntreprise = member?.type === 'entreprise'; const bg = isEntreprise ? 'linear-gradient(135deg, #3730a3, #4338ca)' : 'linear-gradient(135deg, #1e3a8a, #1e40af)'; return (
{memberInitials(member)}
); } function TeamBadge({ size = 28 }) { return (
); } /* ── Composant principal ─────────────────────────────────────── */ export default function UserMenu() { const { user, logout, isAdmin } = useAuth(); const { sidebarCollapsed } = useUi(); const { investisseurs, activeView, activeViewMember, setActiveView } = useInvestisseur(); const navigate = useNavigate(); const [open, setOpen] = useState(false); const [subOpen, setSubOpen] = useState(false); const [popupStyle, setPopupStyle] = useState({}); const [subStyle, setSubStyle] = useState({}); const triggerRef = useRef(null); const popupRef = useRef(null); const subRef = useRef(null); const closeTimer = useRef(null); const famille = investisseurs.filter(i => i.type !== 'entreprise'); const entreprises = investisseurs.filter(i => i.type === 'entreprise'); const POPUP_W = 250; const SUB_W = 230; /* ── Calcul position (fixed = échappe overflow:hidden) ────── */ const computePosition = useCallback(() => { if (!triggerRef.current) return; const r = triggerRef.current.getBoundingClientRect(); // visualViewport est fiable sur Safari mobile (window.innerHeight ne l'est pas) const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight; let mainLeft, mainBottom, mainWidth; if (sidebarCollapsed) { mainLeft = r.right + 8; mainBottom = vh - r.bottom; mainWidth = POPUP_W; } else { mainLeft = r.left; mainBottom = vh - r.top + 6; mainWidth = r.width; } setPopupStyle({ position: 'fixed', left: mainLeft, bottom: mainBottom, width: mainWidth, top: 'auto', right: 'auto', }); setSubStyle({ position: 'fixed', left: mainLeft + (sidebarCollapsed ? POPUP_W : mainWidth) + 6, bottom: mainBottom, width: SUB_W, top: 'auto', right: 'auto', }); }, [sidebarCollapsed]); const openMenu = useCallback(() => { computePosition(); setOpen(true); }, [computePosition]); const closeMenu = useCallback(() => { setOpen(false); setSubOpen(false); }, []); const clearClose = () => clearTimeout(closeTimer.current); const scheduleClose = () => { closeTimer.current = setTimeout(closeMenu, 200); }; /* Fermeture clic extérieur */ useEffect(() => { if (!open) return; const onDown = (e) => { const inTrigger = triggerRef.current?.contains(e.target); const inPopup = popupRef.current?.contains(e.target); const inSub = subRef.current?.contains(e.target); if (!inTrigger && !inPopup && !inSub) closeMenu(); }; document.addEventListener('mousedown', onDown); return () => document.removeEventListener('mousedown', onDown); }, [open, closeMenu]); /* Recalcul si sidebar change ou si visualViewport change (Safari mobile) */ useEffect(() => { if (open) computePosition(); }, [sidebarCollapsed, open, computePosition]); useEffect(() => { if (!open) return; const vp = window.visualViewport; if (!vp) return; vp.addEventListener('resize', computePosition); vp.addEventListener('scroll', computePosition); return () => { vp.removeEventListener('resize', computePosition); vp.removeEventListener('scroll', computePosition); }; }, [open, computePosition]); /* Handlers ouverture */ const onWrapEnter = () => { if (sidebarCollapsed) { clearClose(); openMenu(); } }; const onWrapLeave = () => { if (sidebarCollapsed) scheduleClose(); }; const onTriggerClick = () => { if (!sidebarCollapsed) { open ? closeMenu() : openMenu(); } }; const go = (path) => { closeMenu(); navigate(path); }; const handleLogout = () => { closeMenu(); logout(); navigate('/login'); }; // En mode standalone (webapp ajoutée à l'écran d'accueil iPad), il n'y a // ni bouton reload ni pull-to-refresh natif. On force donc une vraie // navigation réseau (pas juste reload()) avec un paramètre anti-cache, // pour être certain de récupérer l'index.html et les données à jour. const handleForceRefresh = () => { closeMenu(); const url = new URL(window.location.href); url.searchParams.set('_r', Date.now().toString()); window.location.replace(url.toString()); }; const selectView = (v) => { setActiveView(v); closeMenu(); }; /* ── Libellés ────────────────────────────────────────────── */ const viewLabel = activeView === 'all' ? 'Famille et entreprises' : (activeViewMember ? memberLabel(activeViewMember) : 'Famille et entreprises'); const TriggerBadge = activeView === 'all' ? : ; return (
{/* ── Main Popup (portail → échappe tout stacking context) ── */} {open && createPortal(