mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 11:26:40 +02:00
- Neuer Hook useO365ProfileSync: läuft einmalig pro Browser-Session, überschreibt INSIGHT-Profil-Felder mit O365-Daten (firstName, lastName, phone, mobile, city, street, postalCode) — kein UI-Feedback, kein Fehler bricht die UX auf. - AppLayout ruft useO365ProfileSync() auf, sodass die Synchronisation beim Laden der App (nach Login) automatisch startet. - ProfilePage: "↓ O365 übernehmen"-Button überschreibt jetzt alle Felder wo O365 Daten hat (nicht mehr nur leere Felder) — konsistent mit dem Auto-Sync-Verhalten. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
676 lines
23 KiB
TypeScript
676 lines
23 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useAuth } from '../auth/AuthContext';
|
|
import { UserAvatar } from '../components/UserAvatar';
|
|
import api from '../api/client';
|
|
import { useTheme } from '../theme/ThemeContext';
|
|
import { useCrmSettings } from '../crm/settings/CrmSettingsContext';
|
|
import { useO365ProfileSync } from '../hooks/useO365ProfileSync';
|
|
import styles from './AppLayout.module.css';
|
|
|
|
interface ExternalLink {
|
|
id: string;
|
|
label: string;
|
|
url: string;
|
|
sortOrder: number;
|
|
customIcon?: string;
|
|
}
|
|
|
|
/** Favicon ueber Backend-Proxy laden (cached in Redis) */
|
|
function FaviconImg({
|
|
url,
|
|
label,
|
|
customIcon,
|
|
}: {
|
|
url: string;
|
|
label: string;
|
|
customIcon?: string;
|
|
}) {
|
|
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
|
|
const [failed, setFailed] = useState(false);
|
|
|
|
const fetchFavicon = useCallback(async () => {
|
|
try {
|
|
new URL(url);
|
|
const res = await api.get<{ faviconUrl: string | null }>(
|
|
`/settings/favicon?url=${encodeURIComponent(url)}`,
|
|
);
|
|
if (res.data.faviconUrl) {
|
|
setFaviconUrl(res.data.faviconUrl);
|
|
setFailed(false);
|
|
} else {
|
|
setFailed(true);
|
|
}
|
|
} catch {
|
|
setFailed(true);
|
|
}
|
|
}, [url]);
|
|
|
|
useEffect(() => {
|
|
fetchFavicon();
|
|
}, [fetchFavicon]);
|
|
|
|
// Eigenes Icon hat Vorrang
|
|
if (customIcon) {
|
|
return (
|
|
<img
|
|
src={customIcon}
|
|
alt=""
|
|
style={{
|
|
width: 16,
|
|
height: 16,
|
|
objectFit: 'contain',
|
|
borderRadius: 2,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (failed || !faviconUrl) {
|
|
return (
|
|
<span
|
|
style={{
|
|
display: 'flex',
|
|
width: 16,
|
|
height: 16,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: '0.625rem',
|
|
fontWeight: 700,
|
|
background: 'rgba(255,255,255,0.15)',
|
|
borderRadius: 2,
|
|
color: 'rgba(255,255,255,0.8)',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{label.charAt(0).toUpperCase()}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<img
|
|
src={faviconUrl}
|
|
alt=""
|
|
style={{
|
|
width: 16,
|
|
height: 16,
|
|
objectFit: 'contain',
|
|
borderRadius: 2,
|
|
}}
|
|
onError={() => setFailed(true)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const THEME_OPTIONS = [
|
|
{ value: 'light' as const, label: 'Hell', icon: '☀' },
|
|
{ value: 'dark' as const, label: 'Dunkel', icon: '☾' },
|
|
{ value: 'system' as const, label: 'System', icon: '⚙' },
|
|
];
|
|
|
|
export function AppLayout() {
|
|
const { user, logout } = useAuth();
|
|
const navigate = useNavigate();
|
|
const { mode, setMode } = useTheme();
|
|
const { isModuleEnabled } = useCrmSettings();
|
|
|
|
// Silently sync INSIGHT profile from O365 once per browser session
|
|
useO365ProfileSync();
|
|
const isAdmin = user?.role === 'PLATFORM_ADMIN' || user?.role === 'TENANT_ADMIN';
|
|
const anyCrmModuleEnabled =
|
|
isModuleEnabled('contacts') ||
|
|
isModuleEnabled('companies') ||
|
|
isModuleEnabled('deals') ||
|
|
isModuleEnabled('pipelines');
|
|
const [crmOpen, setCrmOpen] = useState(true);
|
|
const [appsOpen, setAppsOpen] = useState(true);
|
|
const [collapsed, setCollapsed] = useState(() => {
|
|
return localStorage.getItem('sidebar-collapsed') === 'true';
|
|
});
|
|
|
|
const toggleCollapsed = () => {
|
|
setCollapsed((prev) => {
|
|
const next = !prev;
|
|
localStorage.setItem('sidebar-collapsed', String(next));
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const { data: externalLinks } = useQuery<ExternalLink[]>({
|
|
queryKey: ['settings', 'external-links'],
|
|
queryFn: async () => {
|
|
const res = await api.get<ExternalLink[]>('/settings/external-links');
|
|
return res.data;
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
const { data: branding } = useQuery<{
|
|
logo: string | null;
|
|
sidebarColor: string | null;
|
|
}>({
|
|
queryKey: ['settings', 'branding'],
|
|
queryFn: async () => {
|
|
const res = await api.get<{
|
|
logo: string | null;
|
|
sidebarColor: string | null;
|
|
}>('/settings/branding');
|
|
return res.data;
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
navigate('/login');
|
|
};
|
|
|
|
return (
|
|
<div className={styles.layout}>
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={`${styles.sidebar} ${collapsed ? styles.sidebarCollapsed : ''}`}
|
|
style={
|
|
branding?.sidebarColor
|
|
? { background: branding.sidebarColor }
|
|
: undefined
|
|
}
|
|
>
|
|
<div className={styles.brand}>
|
|
{!collapsed &&
|
|
(branding?.logo ? (
|
|
<img
|
|
src={branding.logo}
|
|
alt="Logo"
|
|
style={{
|
|
maxHeight: 44,
|
|
maxWidth: 160,
|
|
objectFit: 'contain',
|
|
}}
|
|
/>
|
|
) : (
|
|
<h2>INSIGHT</h2>
|
|
))}
|
|
<button
|
|
className={styles.collapseBtn}
|
|
onClick={toggleCollapsed}
|
|
title={collapsed ? 'Menue ausklappen' : 'Menue einklappen'}
|
|
>
|
|
<svg
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 18 18"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
{collapsed ? (
|
|
<>
|
|
<path d="M3 4.5h12M3 9h12M3 13.5h12" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<path d="M11 4l-5 5 5 5" />
|
|
</>
|
|
)}
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav className={styles.nav}>
|
|
<NavLink
|
|
to="/"
|
|
end
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Dashboard"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M2 6l6-4 6 4v7a1 1 0 01-1 1H3a1 1 0 01-1-1V6z" />
|
|
<path d="M6 14V8h4v6" />
|
|
</svg>
|
|
{!collapsed && 'Dashboard'}
|
|
</NavLink>
|
|
|
|
{/* CRM-Bereich (aufklappbar) — nur sichtbar wenn Module aktiv oder Admin */}
|
|
{(anyCrmModuleEnabled || isAdmin) && (
|
|
<>
|
|
{!collapsed ? (
|
|
<button
|
|
className={styles.navSectionToggle}
|
|
onClick={() => setCrmOpen((p) => !p)}
|
|
>
|
|
<span>CRM</span>
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 12 12"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
className={`${styles.chevron} ${crmOpen ? styles.chevronOpen : ''}`}
|
|
>
|
|
<path d="M3 4.5l3 3 3-3" />
|
|
</svg>
|
|
</button>
|
|
) : (
|
|
<div className={styles.navDivider} />
|
|
)}
|
|
{(crmOpen || collapsed) && (
|
|
<>
|
|
{isModuleEnabled('contacts') && (
|
|
<NavLink
|
|
to="/crm/contacts"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Kontakte"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<circle cx="8" cy="5" r="3" />
|
|
<path d="M2 14c0-2.761 2.686-5 6-5s6 2.239 6 5" />
|
|
</svg>
|
|
{!collapsed && 'Kontakte'}
|
|
</NavLink>
|
|
)}
|
|
{isModuleEnabled('companies') && (
|
|
<NavLink
|
|
to="/crm/companies"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Unternehmen"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="2" y="6" width="12" height="9" rx="1" />
|
|
<path d="M5 6V3a1 1 0 011-1h4a1 1 0 011 1v3" />
|
|
<path d="M5 9h2v2H5zM9 9h2v2H9z" />
|
|
</svg>
|
|
{!collapsed && 'Unternehmen'}
|
|
</NavLink>
|
|
)}
|
|
{isModuleEnabled('deals') && (
|
|
<NavLink
|
|
to="/crm/deals"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Vorgänge"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="1" y="5" width="14" height="9" rx="1" />
|
|
<path d="M5 5V3a2 2 0 012-2h2a2 2 0 012 2v2" />
|
|
<path d="M1 9h14" />
|
|
</svg>
|
|
{!collapsed && 'Vorgänge'}
|
|
</NavLink>
|
|
)}
|
|
{isModuleEnabled('pipelines') && (
|
|
<NavLink
|
|
to="/crm/pipelines"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Pipelines"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="1" y="2" width="4" height="12" rx="0.5" />
|
|
<rect x="6" y="2" width="4" height="8" rx="0.5" />
|
|
<rect x="11" y="2" width="4" height="10" rx="0.5" />
|
|
</svg>
|
|
{!collapsed && 'Pipelines'}
|
|
</NavLink>
|
|
)}
|
|
{isModuleEnabled('deals') && (
|
|
<NavLink
|
|
to="/crm/forecast"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Prognose"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<polyline points="1 12 5 7 9 10 15 3" />
|
|
<polyline points="11 3 15 3 15 7" />
|
|
</svg>
|
|
{!collapsed && 'Prognose'}
|
|
</NavLink>
|
|
)}
|
|
{isModuleEnabled('deals') && (
|
|
<NavLink
|
|
to="/crm/kanban"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Kanban"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="1" y="2" width="4" height="9" rx="0.5" />
|
|
<rect x="6" y="2" width="4" height="6" rx="0.5" />
|
|
<rect x="11" y="2" width="4" height="11" rx="0.5" />
|
|
</svg>
|
|
{!collapsed && 'Kanban'}
|
|
</NavLink>
|
|
)}
|
|
<NavLink
|
|
to="/crm/office365"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="Office 365"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="1" y="1" width="6.5" height="6.5" rx="1" />
|
|
<rect x="8.5" y="1" width="6.5" height="6.5" rx="1" />
|
|
<rect x="1" y="8.5" width="6.5" height="6.5" rx="1" />
|
|
<rect x="8.5" y="8.5" width="6.5" height="6.5" rx="1" />
|
|
</svg>
|
|
{!collapsed && 'Office 365'}
|
|
</NavLink>
|
|
{/* CRM Einstellungen (nur Admins) */}
|
|
{isAdmin && (
|
|
<NavLink
|
|
to="/crm/settings"
|
|
className={({ isActive }) =>
|
|
`${styles.navLink} ${isActive ? styles.active : ''}`
|
|
}
|
|
title="CRM Einstellungen"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<circle cx="8" cy="8" r="2" />
|
|
<path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41" />
|
|
</svg>
|
|
{!collapsed && 'Einstellungen'}
|
|
</NavLink>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Externe Links (aufklappbar) */}
|
|
{externalLinks && externalLinks.length > 0 && (
|
|
<>
|
|
{!collapsed ? (
|
|
<button
|
|
className={styles.navSectionToggle}
|
|
onClick={() => setAppsOpen((p) => !p)}
|
|
>
|
|
<span>Anwendungen</span>
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 12 12"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
className={`${styles.chevron} ${appsOpen ? styles.chevronOpen : ''}`}
|
|
>
|
|
<path d="M3 4.5l3 3 3-3" />
|
|
</svg>
|
|
</button>
|
|
) : (
|
|
<div className={styles.navDivider} />
|
|
)}
|
|
{(appsOpen || collapsed) &&
|
|
externalLinks.map((link) => (
|
|
<a
|
|
key={link.id}
|
|
href={link.url}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
window.open(link.url, link.label, 'popup,noopener');
|
|
}}
|
|
className={styles.navLink}
|
|
title={link.label}
|
|
>
|
|
<FaviconImg url={link.url} label={link.label} customIcon={link.customIcon} />
|
|
{!collapsed && (
|
|
<>
|
|
{link.label}
|
|
<svg
|
|
width="10"
|
|
height="10"
|
|
viewBox="0 0 10 10"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
style={{ marginLeft: 'auto', opacity: 0.4 }}
|
|
>
|
|
<path d="M6 1h3v3" />
|
|
<path d="M4 6l5-5" />
|
|
</svg>
|
|
</>
|
|
)}
|
|
</a>
|
|
))}
|
|
</>
|
|
)}
|
|
</nav>
|
|
|
|
{/* Admin-Link (nur PLATFORM_ADMIN) */}
|
|
{user?.role === 'PLATFORM_ADMIN' && (
|
|
<div className={styles.adminSection}>
|
|
<NavLink
|
|
to="/admin/users"
|
|
className={({ isActive }) =>
|
|
`${styles.adminLink} ${isActive ? styles.adminLinkActive : ''}`
|
|
}
|
|
title="Administration"
|
|
style={({ isActive }) => {
|
|
const isAdminPath =
|
|
window.location.pathname.startsWith('/admin');
|
|
return isAdminPath && !isActive
|
|
? {
|
|
color: 'white',
|
|
background: 'rgba(96, 165, 250, 0.15)',
|
|
}
|
|
: {};
|
|
}}
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
{!collapsed && 'Administration'}
|
|
</NavLink>
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.userInfo}>
|
|
{collapsed ? (
|
|
<div
|
|
className={styles.userProfile}
|
|
onClick={() => navigate('/profile')}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') navigate('/profile');
|
|
}}
|
|
title={`${user?.firstName} ${user?.lastName}`}
|
|
>
|
|
<UserAvatar
|
|
firstName={user?.firstName ?? ''}
|
|
lastName={user?.lastName ?? ''}
|
|
avatar={user?.avatar}
|
|
size={32}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div
|
|
className={styles.userProfile}
|
|
onClick={() => navigate('/profile')}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ')
|
|
navigate('/profile');
|
|
}}
|
|
>
|
|
<div className={styles.userProfileInner}>
|
|
<UserAvatar
|
|
firstName={user?.firstName ?? ''}
|
|
lastName={user?.lastName ?? ''}
|
|
avatar={user?.avatar}
|
|
size={36}
|
|
/>
|
|
<div className={styles.userDetails}>
|
|
<div className={styles.userName}>
|
|
{user?.firstName} {user?.lastName}
|
|
</div>
|
|
<div className={styles.userEmail}>{user?.email}</div>
|
|
</div>
|
|
</div>
|
|
<div className={styles.profileHint}>Zum Profil →</div>
|
|
</div>
|
|
<button className={styles.logoutBtn} onClick={handleLogout}>
|
|
Abmelden
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Theme Toggle (unter dem Profil) */}
|
|
<div className={styles.themeToggle}>
|
|
{collapsed ? (
|
|
<button
|
|
className={styles.themeBtn}
|
|
onClick={() => {
|
|
const next =
|
|
mode === 'light'
|
|
? 'dark'
|
|
: mode === 'dark'
|
|
? 'system'
|
|
: 'light';
|
|
setMode(next);
|
|
}}
|
|
title={`Theme: ${THEME_OPTIONS.find((o) => o.value === mode)?.label}`}
|
|
>
|
|
{THEME_OPTIONS.find((o) => o.value === mode)?.icon}
|
|
</button>
|
|
) : (
|
|
<div className={styles.themeBtnGroup}>
|
|
{THEME_OPTIONS.map((opt) => (
|
|
<button
|
|
key={opt.value}
|
|
className={`${styles.themeOption} ${mode === opt.value ? styles.themeOptionActive : ''}`}
|
|
onClick={() => setMode(opt.value)}
|
|
title={opt.label}
|
|
>
|
|
<span className={styles.themeIcon}>{opt.icon}</span>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main
|
|
className={styles.main}
|
|
style={{
|
|
marginLeft: collapsed ? 60 : undefined,
|
|
}}
|
|
>
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|