1import React, { useState, useEffect } from 'react';
2import { apiFetch } from '../../lib/api';
3import TenantSelector from '../TenantSelector';
4import { useNavigate, useLocation } from 'react-router-dom';
5import './MainLayout.css';
6import '../../styles/themes.css';
7import '../../styles/navigation.css';
8import ThemeSelector from '../ThemeSelector';
10// -------------------------
12// -------------------------
13function parseJwt(token) {
15 const payload = token.split('.')[1];
16 return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
22function MainLayout({ children }) {
23 // -------------------------
24 // Main Content Handler (reloads on tenant change)
25 // -------------------------
26 const token = localStorage.getItem('token');
27 const jwtPayload = token ? parseJwt(token) : null;
28 const tenantId = jwtPayload?.tenant_id || '';
32 // -------------------------
33 // JWT + Root Detection
34 // -------------------------
35 // token and jwtPayload already declared above
39 (jwtPayload.role === 'root' || jwtPayload.role === 'msp');
40 const isImpersonated = jwtPayload && jwtPayload.impersonated === true;
41 const [isRoot, setIsRoot] = useState(jwtIsRoot);
43 // Validate backend root status if not JWT-root
45 async function checkRootStatus() {
46 if (!token || jwtIsRoot) return;
48 const res = await apiFetch('/tenants', {
50 Authorization: `Bearer ${token}`,
51 'X-Tenant-Subdomain': 'admin'
60 }, [token, jwtIsRoot]);
62 // -------------------------
63 // Tenant Impersonation
64 // -------------------------
66 const handleTenantChange = async (tid) => {
68 const res = await apiFetch(`/auth/impersonate`, {
70 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
71 body: JSON.stringify({ tenant_id: tid })
74 const data = await res.json();
76 localStorage.setItem('token', data.token);
77 window.location.reload();
80 alert('Failed to impersonate tenant');
83 alert('Error impersonating tenant');
87 // -------------------------
88 // Navigation + Layout State
89 // -------------------------
90 const navigate = useNavigate();
91 const location = useLocation();
92 const [activeTab, setActiveTab] = useState('dashboard');
93 const [showNotifications, setShowNotifications] = useState(false);
95 const [companyName, setCompanyName] = useState('RMM+PSA');
98 { id: 'dashboard', label: 'Dashboard', icon: 'dashboard', path: '/dashboard' },
99 { id: 'tickets', label: 'Tickets', icon: 'confirmation_number', path: '/tickets' },
100 { id: 'customers', label: 'Customers', icon: 'groups', path: '/customers' },
101 { id: 'monitoring', label: 'Monitoring', icon: 'monitoring', path: '/monitoring' },
102 { id: 'knowledge-base', label: 'Knowledge Base', icon: 'description', path: '/knowledge-base' },
103 { id: 'contracts', label: 'Contracts', icon: 'contract_edit', path: '/contracts' },
104 { id: 'products', label: 'Products', icon: 'inventory_2', path: '/products' },
105 { id: 'services', label: 'Services', icon: 'cloud_circle', path: '/services' },
106 { id: 'orders', label: 'Orders', icon: 'shopping_cart', path: '/orders' },
107 { id: 'purchase-orders', label: 'Purchase Orders', icon: 'local_shipping', path: '/purchase-orders' },
108 { id: 'invoices', label: 'Invoices', icon: 'receipt_long', path: '/invoices' },
109 { id: 'reports', label: 'Reports', icon: 'analytics', path: '/reports' },
110 { id: 'integrations', label: 'Integrations', icon: 'extension', path: '/integrations' },
111 { id: 'settings', label: 'Settings', icon: 'settings', path: '/settings' }
114 // Only show Tenants menu if root
115 const navItemsWithTenants = isRoot
116 ? [...navItems.slice(0, 12), { id: 'tenants', label: 'Tenants', icon: 'domain', path: '/tenants' }, ...navItems.slice(12)]
119 // -------------------------
121 // -------------------------
122 const [notifications, setNotifications] = useState([]);
124 const loadNotifications = async () => {
127 const res = await apiFetch('/notifications', {
128 headers: { Authorization: `Bearer ${token}` }
131 const data = await res.json();
132 setNotifications(data.slice(0, 10));
140 // Listen for new notifications
141 const handleNotificationCreated = () => {
145 window.addEventListener('notificationCreated', handleNotificationCreated);
148 window.removeEventListener('notificationCreated', handleNotificationCreated);
152 // -------------------------
153 // Active Tab Detection
154 // -------------------------
156 const current = navItems.find((n) =>
157 location.pathname.startsWith(n.path)
159 setActiveTab(current ? current.id : 'dashboard');
160 }, [location.pathname, navItems]);
162 // -------------------------
163 // Fetch Company Name
164 // -------------------------
166 async function fetchCompanyName() {
169 const res = await apiFetch('/settings', {
170 headers: { Authorization: `Bearer ${token}` }
173 const settings = await res.json();
174 if (settings?.general?.companyName) {
175 setCompanyName(settings.general.companyName);
178 console.error('Failed to fetch settings:', err);
182 const handleSettingsUpdate = (event) => {
183 if (event.detail?.general?.companyName) {
184 setCompanyName(event.detail.general.companyName);
187 const handleTenantChangeEvent = () => fetchCompanyName();
188 window.addEventListener('settingsUpdated', handleSettingsUpdate);
189 window.addEventListener('tenantChanged', handleTenantChangeEvent);
191 window.removeEventListener('settingsUpdated', handleSettingsUpdate);
192 window.removeEventListener('tenantChanged', handleTenantChangeEvent);
194 }, [token, tenantId]);
196 // -------------------------
198 // -------------------------
199 const handleLogout = () => {
200 localStorage.removeItem('token');
204 // -------------------------
205 // MAIN CONTENT RENDER
206 // -------------------------
207 const renderContent = () => (
208 <div className={`layout${isRoot ? ' with-tenant-selector' : ''}`}>
209 <header className="header">
210 <div className="logo">
211 <span className="logo-text">{companyName}</span>
214 <div className="header-right">
217 {/* Notifications */}
218 <div className="notifications">
220 className="notification-btn"
221 onClick={() => setShowNotifications(!showNotifications)}
223 <span className="material-symbols-outlined">notifications</span>
224 {notifications.filter((n) => !n.is_read).length > 0 && (
225 <span className="badge">
226 {notifications.filter((n) => !n.is_read).length}
231 {showNotifications && (
232 <div className="notifications-dropdown">
233 {notifications.map((note) => (
235 key={note.notification_id}
236 className={`notification-item ${note.type}`}
238 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
239 <div>{note.title}</div>
240 <div style={{ fontSize: 12 }}>
241 {new Date(note.created_at).toLocaleString()}
244 <div style={{ marginTop: 6 }}>{note.message}</div>
248 onClick={async () => {
251 `/notifications/${note.notification_id}/mark-read`,
254 headers: { Authorization: `Bearer ${token}` }
257 setNotifications((prev) =>
259 n.notification_id === note.notification_id
260 ? { ...n, is_read: true }
274 <div style={{ padding: 8, textAlign: 'center' }}>
275 <a href="/notifications">View all</a>
282 <div className="user-info">
283 <span className="user-name">
284 {jwtPayload?.name || `User ${jwtPayload?.user_id || ''}`}
285 {jwtPayload?.role && ` (${jwtPayload.role})`}
287 <button onClick={handleLogout} className="logout-btn">
294 <div className="main">
295 {/* Sidebar Navigation */}
296 <nav className="sidebar">
297 {navItemsWithTenants.map((item) => (
300 className={`nav-item ${activeTab === item.id ? 'active' : ''}`}
302 setActiveTab(item.id);
306 <span className="nav-icon material-symbols-outlined">{item.icon}</span>
307 <span className="nav-label">{item.label}</span>
313 <main className="content" key={tenantId}>
314 {typeof children === 'function'
315 ? children({ tenantId, onTenantChange: handleTenantChange })
322 // -------------------------
324 // -------------------------
326 <div className="main-layout-container">
327 {(isRoot || isImpersonated) && (
328 <div className="tenant-selector-bar">
330 selectedTenantId={tenantId}
331 onTenantChange={handleTenantChange}
340export default MainLayout;