Mise en place des workspaces
This commit is contained in:
1 parent
be3c93897b
commit
6b6cad7cd6
19 files changed
+1273
-56
No files matched your search
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api.js';
|
||||
import { useInvestisseur } from '../context/InvestisseurContext.jsx';
|
||||
import { useWorkspace } from '../context/WorkspaceContext.jsx';
|
||||
import { useAuth } from '../context/AuthContext.jsx';
|
||||
import { useUi } from '../context/UiContext.jsx';
|
||||
import Logo from './Logo.jsx';
|
||||
@@ -253,6 +254,10 @@ const IconBurger = () => (
|
||||
export default function Layout() {
|
||||
const { sidebarCollapsed, setSidebarCollapsed, toggleSidebar, displayMode, setDisplayMode } = useUi();
|
||||
const { hasFrais } = useAuth();
|
||||
const { active: activeWorkspace } = useWorkspace() || {};
|
||||
// Gating de nav par workspace (Phase 4, étape 0) : fail-open tant que
|
||||
// activeWorkspace n'est pas chargé, cf. App.jsx (RequireWorkspace).
|
||||
const isCrowdlendingActive = !activeWorkspace || activeWorkspace.slug === 'crowdlending';
|
||||
const navigate = useNavigate();
|
||||
const [navIcons, setNavIcons] = useState({});
|
||||
const [aiEnabled, setAiEnabled] = useState(false);
|
||||
@@ -299,7 +304,7 @@ export default function Layout() {
|
||||
<div className="sidebar-brand-logo">
|
||||
<Logo size={34} />
|
||||
</div>
|
||||
<span className="sidebar-brand-text">Crowdlending</span>
|
||||
<span className="sidebar-brand-text">{activeWorkspace?.libelle_menu || 'Crowdlending'}</span>
|
||||
|
||||
{/* Réduire (visible seulement en mode étendu) */}
|
||||
<button
|
||||
@@ -327,30 +332,34 @@ export default function Layout() {
|
||||
<NavLink to="/" end title="Tableau de bord">
|
||||
<NavIcon libFilename={navIcons.dashboard} Fallback={IconDashboard} /><span className="nav-label">Tableau de bord</span>
|
||||
</NavLink>
|
||||
<NavLink to="/plateformes" title="Plateformes">
|
||||
<NavIcon libFilename={navIcons.plateforme} Fallback={IconInvestments} /><span className="nav-label">Plateformes</span>
|
||||
</NavLink>
|
||||
<NavLink to="/investissements" title="Investissements">
|
||||
<NavIcon libFilename={navIcons.investissement} Fallback={IconInvestments} /><span className="nav-label">Investissements</span>
|
||||
</NavLink>
|
||||
<NavLink to="/depots-retraits" title="Dépôts / Retraits">
|
||||
<NavIcon libFilename={navIcons['depots-retraits']} Fallback={IconDeposits} /><span className="nav-label">Dépôts / Retraits</span>
|
||||
</NavLink>
|
||||
<NavLink to="/remboursements" title="Remboursements">
|
||||
<NavIcon libFilename={navIcons.remboursement} Fallback={IconRepayments} /><span className="nav-label">Remboursements</span>
|
||||
</NavLink>
|
||||
{hasFrais && (
|
||||
<NavLink to="/frais" title="Frais">
|
||||
<NavIcon libFilename={navIcons.frais} Fallback={IconFrais} /><span className="nav-label">Frais</span>
|
||||
</NavLink>
|
||||
)}
|
||||
<NavLink to="/taxreport" title="Fiscalité">
|
||||
<NavIcon libFilename={navIcons.tax} Fallback={IconFlatTax} /><span className="nav-label">Fiscalité</span>
|
||||
</NavLink>
|
||||
{aiEnabled && (
|
||||
<NavLink to="/assistant-ia" title="Assistant IA">
|
||||
<NavIcon libFilename={navIcons.assistantIa} Fallback={IconAssistantIa} /><span className="nav-label">Assistant IA</span>
|
||||
</NavLink>
|
||||
{isCrowdlendingActive && (
|
||||
<>
|
||||
<NavLink to="/plateformes" title="Plateformes">
|
||||
<NavIcon libFilename={navIcons.plateforme} Fallback={IconInvestments} /><span className="nav-label">Plateformes</span>
|
||||
</NavLink>
|
||||
<NavLink to="/investissements" title="Investissements">
|
||||
<NavIcon libFilename={navIcons.investissement} Fallback={IconInvestments} /><span className="nav-label">Investissements</span>
|
||||
</NavLink>
|
||||
<NavLink to="/depots-retraits" title="Dépôts / Retraits">
|
||||
<NavIcon libFilename={navIcons['depots-retraits']} Fallback={IconDeposits} /><span className="nav-label">Dépôts / Retraits</span>
|
||||
</NavLink>
|
||||
<NavLink to="/remboursements" title="Remboursements">
|
||||
<NavIcon libFilename={navIcons.remboursement} Fallback={IconRepayments} /><span className="nav-label">Remboursements</span>
|
||||
</NavLink>
|
||||
{hasFrais && (
|
||||
<NavLink to="/frais" title="Frais">
|
||||
<NavIcon libFilename={navIcons.frais} Fallback={IconFrais} /><span className="nav-label">Frais</span>
|
||||
</NavLink>
|
||||
)}
|
||||
<NavLink to="/taxreport" title="Fiscalité">
|
||||
<NavIcon libFilename={navIcons.tax} Fallback={IconFlatTax} /><span className="nav-label">Fiscalité</span>
|
||||
</NavLink>
|
||||
{aiEnabled && (
|
||||
<NavLink to="/assistant-ia" title="Assistant IA">
|
||||
<NavIcon libFilename={navIcons.assistantIa} Fallback={IconAssistantIa} /><span className="nav-label">Assistant IA</span>
|
||||
</NavLink>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</nav>
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { useWorkspace } from '../context/WorkspaceContext.jsx';
|
||||
import { memberInitials, memberLabel } from '../utils/format.js';
|
||||
|
||||
/* ── Icons ───────────────────────────────────────────────────── */
|
||||
@@ -47,6 +48,9 @@ function IconCheck() {
|
||||
function IconTeam() {
|
||||
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>;
|
||||
}
|
||||
function IconLayers() {
|
||||
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>;
|
||||
}
|
||||
|
||||
/* ── Avatars ─────────────────────────────────────────────────── */
|
||||
function UserAvatar({ user, size = 36 }) {
|
||||
@@ -100,15 +104,31 @@ function TeamBadge({ size = 28 }) {
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceBadge({ size = 28 }) {
|
||||
return (
|
||||
<div className="team-badge" style={{
|
||||
width: size, height: size, borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,.25)',
|
||||
color: '#fff',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<IconLayers />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Composant principal ─────────────────────────────────────── */
|
||||
export default function UserMenu() {
|
||||
const { user, logout, isAdmin } = useAuth();
|
||||
const { sidebarCollapsed } = useUi();
|
||||
const { investisseurs, activeView, activeViewMember, setActiveView } = useInvestisseur();
|
||||
const { activeWorkspaces, active: activeWorkspace, setActiveWorkspace, hasMultipleWorkspaces } = useWorkspace() || {};
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [subOpen, setSubOpen] = useState(false);
|
||||
const [activeSubPanel, setActiveSubPanel] = useState(null); // null | 'profile' | 'workspace'
|
||||
const [popupStyle, setPopupStyle] = useState({});
|
||||
const [subStyle, setSubStyle] = useState({});
|
||||
|
||||
@@ -154,7 +174,7 @@ export default function UserMenu() {
|
||||
}, [sidebarCollapsed]);
|
||||
|
||||
const openMenu = useCallback(() => { computePosition(); setOpen(true); }, [computePosition]);
|
||||
const closeMenu = useCallback(() => { setOpen(false); setSubOpen(false); }, []);
|
||||
const closeMenu = useCallback(() => { setOpen(false); setActiveSubPanel(null); }, []);
|
||||
|
||||
const clearClose = () => clearTimeout(closeTimer.current);
|
||||
const scheduleClose = () => { closeTimer.current = setTimeout(closeMenu, 200); };
|
||||
@@ -211,6 +231,11 @@ export default function UserMenu() {
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const selectWorkspace = (id) => {
|
||||
setActiveWorkspace?.(id);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
/* ── Libellés ────────────────────────────────────────────── */
|
||||
const viewLabel = activeView === 'all'
|
||||
? 'Famille et entreprises'
|
||||
@@ -248,12 +273,31 @@ export default function UserMenu() {
|
||||
|
||||
<div className="user-menu-sep" />
|
||||
|
||||
{/* Section Espace de travail (visible uniquement si l'utilisateur
|
||||
a plusieurs workspaces actifs) */}
|
||||
{hasMultipleWorkspaces && (
|
||||
<>
|
||||
<div className="user-menu-section-header">Espace de travail</div>
|
||||
<button
|
||||
className={`user-menu-item user-menu-vue-row${activeSubPanel === 'workspace' ? ' active' : ''}`}
|
||||
role="menuitem"
|
||||
onClick={() => setActiveSubPanel(p => p === 'workspace' ? null : 'workspace')}
|
||||
>
|
||||
<WorkspaceBadge size={24} />
|
||||
<span className="user-menu-vue-name">{activeWorkspace?.libelle_menu || activeWorkspace?.nom || 'Espace de travail'}</span>
|
||||
<span style={{ color: '#4a6490', display: 'flex' }}><IconChevronRight /></span>
|
||||
</button>
|
||||
|
||||
<div className="user-menu-sep" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Section Vue du profil */}
|
||||
<div className="user-menu-section-header">Vue du profil</div>
|
||||
<button
|
||||
className={`user-menu-item user-menu-vue-row${subOpen ? ' active' : ''}`}
|
||||
className={`user-menu-item user-menu-vue-row${activeSubPanel === 'profile' ? ' active' : ''}`}
|
||||
role="menuitem"
|
||||
onClick={() => setSubOpen(s => !s)}
|
||||
onClick={() => setActiveSubPanel(p => p === 'profile' ? null : 'profile')}
|
||||
>
|
||||
{activeView === 'all'
|
||||
? <TeamBadge size={24} />
|
||||
@@ -295,8 +339,34 @@ export default function UserMenu() {
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* ── Sub-panel Espace de travail (portail) ───────────────── */}
|
||||
{open && activeSubPanel === 'workspace' && createPortal(
|
||||
<div
|
||||
ref={subRef}
|
||||
className="user-menu-subpanel"
|
||||
style={subStyle}
|
||||
onMouseEnter={clearClose}
|
||||
onMouseLeave={() => { if (sidebarCollapsed) scheduleClose(); }}
|
||||
>
|
||||
<div className="user-menu-subpanel-title">Espace de travail</div>
|
||||
|
||||
{(activeWorkspaces || []).map(ws => (
|
||||
<button
|
||||
key={ws.id}
|
||||
className={`user-menu-profile-item${activeWorkspace?.id === ws.id ? ' selected' : ''}`}
|
||||
onClick={() => selectWorkspace(ws.id)}
|
||||
>
|
||||
<WorkspaceBadge size={26} />
|
||||
<span className="user-menu-profile-name">{ws.libelle_menu || ws.nom}</span>
|
||||
{activeWorkspace?.id === ws.id && <IconCheck />}
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* ── Sub-panel Vue du profil (portail) ───────────────────── */}
|
||||
{open && subOpen && createPortal(
|
||||
{open && activeSubPanel === 'profile' && createPortal(
|
||||
<div
|
||||
ref={subRef}
|
||||
className="user-menu-subpanel"
|
||||
|
||||
Reference in new issue
Block a user