Mise en place des workspaces

This commit is contained in:
ocroguennec committed 2026-09-08 12:33:04 +02:00
1 parent be3c93897b
commit 6b6cad7cd6
19 files changed
+1273 -56

No files matched your search

+34 -25
View File
@@ -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>
+75 -5
View File
@@ -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"