ptechcore_ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.js +28 -0
- package/index.html +78 -0
- package/package.json +42 -0
- package/postcss.config.js +6 -0
- package/src/App.tsx +156 -0
- package/src/assets/imgs/login_illustration.png +0 -0
- package/src/components/common/Buttons.tsx +39 -0
- package/src/components/common/Cards.tsx +18 -0
- package/src/components/common/FDrawer.tsx +2448 -0
- package/src/components/common/FDrawer.types.ts +191 -0
- package/src/components/common/Inputs.tsx +409 -0
- package/src/components/common/Modals.tsx +41 -0
- package/src/components/common/Navigations.tsx +0 -0
- package/src/components/common/Toast.tsx +0 -0
- package/src/components/demo/ToastDemo.tsx +73 -0
- package/src/components/layout/Header.tsx +202 -0
- package/src/components/layout/ModernDoubleSidebarLayout.tsx +727 -0
- package/src/components/layout/PrivateLayout.tsx +52 -0
- package/src/components/layout/Sidebar.tsx +182 -0
- package/src/components/ui/Toast.tsx +93 -0
- package/src/contexts/SessionContext.tsx +77 -0
- package/src/contexts/ThemeContext.tsx +58 -0
- package/src/contexts/ToastContext.tsx +94 -0
- package/src/index.css +3 -0
- package/src/main.tsx +10 -0
- package/src/models/Organization.ts +47 -0
- package/src/models/Plan.ts +42 -0
- package/src/models/User.ts +23 -0
- package/src/pages/Analytics.tsx +101 -0
- package/src/pages/CreateOrganization.tsx +215 -0
- package/src/pages/Dashboard.tsx +15 -0
- package/src/pages/Home.tsx +12 -0
- package/src/pages/Profile.tsx +313 -0
- package/src/pages/Settings.tsx +382 -0
- package/src/pages/Team.tsx +180 -0
- package/src/pages/auth/Login.tsx +140 -0
- package/src/pages/auth/Register.tsx +302 -0
- package/src/pages/organizations/DetailEntity.tsx +1002 -0
- package/src/pages/organizations/DetailOrganizations.tsx +1629 -0
- package/src/pages/organizations/ListOrganizations.tsx +270 -0
- package/src/pages/pricings/CartPlan.tsx +486 -0
- package/src/pages/pricings/ListPricing.tsx +321 -0
- package/src/pages/users/CreateUser.tsx +450 -0
- package/src/pages/users/ListUsers.tsx +0 -0
- package/src/services/AuthServices.ts +94 -0
- package/src/services/OrganizationServices.ts +61 -0
- package/src/services/PlanSubscriptionServices.tsx +137 -0
- package/src/services/UserServices.ts +36 -0
- package/src/services/api.ts +64 -0
- package/src/styles/theme.ts +383 -0
- package/src/utils/utils.ts +48 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +158 -0
- package/tsconfig.app.json +24 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.ts +10 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
LayoutDashboard, FileText, Users, CreditCard, TrendingUp,
|
|
6
|
+
Settings, ChevronLeft, ChevronRight, Search, Bell, User,
|
|
7
|
+
LogOut, Menu, X, Moon, Sun, Palette, Building2,
|
|
8
|
+
Calculator, PiggyBank, FileBarChart, Shield, Package,
|
|
9
|
+
DollarSign, BarChart3, Briefcase, Receipt, UserCheck,
|
|
10
|
+
Database, Globe, HelpCircle, Activity, Lock, AlertTriangle,
|
|
11
|
+
CheckCircle, Clock, FileCheck, Bot, ScanLine,
|
|
12
|
+
MessageSquare, Smartphone, Workflow, RefreshCw, Wifi,
|
|
13
|
+
Eye, Target, BookOpen, Archive, Download,
|
|
14
|
+
Upload, Save, FolderOpen, Home, ChevronDown, Link, PieChart,
|
|
15
|
+
Video, Calendar, Folder
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import { cn } from '../../utils/utils';
|
|
18
|
+
import { useTheme } from '../../contexts/ThemeContext';
|
|
19
|
+
|
|
20
|
+
export interface MenuItem {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon: React.ReactNode;
|
|
24
|
+
path?: string;
|
|
25
|
+
badge?: string | number;
|
|
26
|
+
submenu?: MenuItem[];
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Notification {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
message: string;
|
|
34
|
+
type: 'info' | 'warning' | 'error' | 'success';
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
read: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
interface PrivateLayoutProps {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
module_name: string;
|
|
43
|
+
primaryMenuItems: MenuItem[];
|
|
44
|
+
secondaryMenuItems: Record<string, MenuItem[]>;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const RewiseLayout: React.FC<PrivateLayoutProps> = ({ children, module_name='Rewise', primaryMenuItems, secondaryMenuItems }) => {
|
|
49
|
+
const location = useLocation();
|
|
50
|
+
const navigate = useNavigate();
|
|
51
|
+
const { theme, themeType, setTheme } = useTheme();
|
|
52
|
+
|
|
53
|
+
const [primaryCollapsed, setPrimaryCollapsed] = useState(false);
|
|
54
|
+
const [secondaryCollapsed, setSecondaryCollapsed] = useState(false);
|
|
55
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
56
|
+
const [selectedModule, setSelectedModule] = useState('dashboard');
|
|
57
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
58
|
+
const [showNotifications, setShowNotifications] = useState(false);
|
|
59
|
+
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
60
|
+
const [showThemeMenu, setShowThemeMenu] = useState(false);
|
|
61
|
+
const [notifications, setNotifications] = useState<Notification[]>([
|
|
62
|
+
{
|
|
63
|
+
id: '1',
|
|
64
|
+
title: 'Nouvelle facture',
|
|
65
|
+
message: '3 nouvelles factures en attente de validation',
|
|
66
|
+
type: 'info',
|
|
67
|
+
timestamp: new Date(),
|
|
68
|
+
read: false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: '2',
|
|
72
|
+
title: 'Clôture mensuelle',
|
|
73
|
+
message: 'La clôture de janvier est prête',
|
|
74
|
+
type: 'success',
|
|
75
|
+
timestamp: new Date(),
|
|
76
|
+
read: false
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
// Navigation clavier
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
83
|
+
// Alt + M pour toggle menu mobile
|
|
84
|
+
if (e.altKey && e.key === 'm') {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
setMobileMenuOpen(prev => !prev);
|
|
87
|
+
}
|
|
88
|
+
// Alt + S pour recherche
|
|
89
|
+
if (e.altKey && e.key === 's') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
document.getElementById('global-search')?.focus();
|
|
92
|
+
}
|
|
93
|
+
// Escape pour fermer les menus
|
|
94
|
+
if (e.key === 'Escape') {
|
|
95
|
+
setShowNotifications(false);
|
|
96
|
+
setShowUserMenu(false);
|
|
97
|
+
setShowThemeMenu(false);
|
|
98
|
+
setMobileMenuOpen(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
103
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// Détection automatique du module actif basé sur l'URL
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const path = location.pathname;
|
|
111
|
+
const moduleMatch = path.match(/^\/([^/]+)/);
|
|
112
|
+
if (moduleMatch) {
|
|
113
|
+
const moduleId = moduleMatch[1];
|
|
114
|
+
// Mapping des routes vers les IDs de modules
|
|
115
|
+
const routeMapping: Record<string, string> = {
|
|
116
|
+
'dashboard': 'dashboard',
|
|
117
|
+
'organizations': 'organizations',
|
|
118
|
+
'entities': 'entities',
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
};
|
|
122
|
+
setSelectedModule(routeMapping[moduleId] || 'dashboard');
|
|
123
|
+
}
|
|
124
|
+
}, [location]);
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
const handleThemeChange = (type: 'elegant' | 'fintech' | 'minimalist') => {
|
|
130
|
+
setTheme(type);
|
|
131
|
+
setShowThemeMenu(false);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const isActive = (path: string) => location.pathname === path;
|
|
135
|
+
const isModuleActive = (moduleId: string) => selectedModule === moduleId;
|
|
136
|
+
|
|
137
|
+
// Breadcrumbs
|
|
138
|
+
const getBreadcrumbs = () => {
|
|
139
|
+
const paths = location.pathname.split('/').filter(Boolean);
|
|
140
|
+
const breadcrumbs = [{ label: 'Accueil', path: '/' }];
|
|
141
|
+
|
|
142
|
+
paths.forEach((path, index) => {
|
|
143
|
+
const fullPath = '/' + paths.slice(0, index + 1).join('/');
|
|
144
|
+
const module = primaryMenuItems.find(m => m.id === path);
|
|
145
|
+
const label = module ? module.label : path.charAt(0).toUpperCase() + path.slice(1);
|
|
146
|
+
breadcrumbs.push({ label, path: fullPath });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return breadcrumbs;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Mark notification as read
|
|
153
|
+
const markNotificationAsRead = (id: string) => {
|
|
154
|
+
setNotifications(prev =>
|
|
155
|
+
prev.map(n => n.id === id ? { ...n, read: true } : n)
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="flex h-screen bg-[var(--color-background)] overflow-hidden">
|
|
161
|
+
{/* Skip to main content (Accessibility) */}
|
|
162
|
+
<a
|
|
163
|
+
href="#main-content"
|
|
164
|
+
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 z-50 bg-[var(--color-primary)] text-[var(--color-background)] px-4 py-2 rounded"
|
|
165
|
+
>
|
|
166
|
+
Aller au contenu principal
|
|
167
|
+
</a>
|
|
168
|
+
|
|
169
|
+
{/* Primary Sidebar */}
|
|
170
|
+
<aside
|
|
171
|
+
className={cn(
|
|
172
|
+
'hidden lg:flex flex-col bg-[var(--color-sidebar-bg)] transition-all duration-300',
|
|
173
|
+
primaryCollapsed ? 'w-20' : 'w-64'
|
|
174
|
+
)}
|
|
175
|
+
role="navigation"
|
|
176
|
+
aria-label="Navigation principale"
|
|
177
|
+
>
|
|
178
|
+
{/* Logo */}
|
|
179
|
+
<div className="h-16 flex items-center justify-between px-4 border-b border-[var(--color-sidebar-border)]">
|
|
180
|
+
<div className={cn(
|
|
181
|
+
'flex items-center gap-3',
|
|
182
|
+
primaryCollapsed && 'justify-center'
|
|
183
|
+
)}>
|
|
184
|
+
<div className="w-10 h-10 bg-[var(--color-primary)] rounded-lg flex items-center justify-center">
|
|
185
|
+
<span className="text-[var(--color-background)] font-bold text-xl">W</span>
|
|
186
|
+
</div>
|
|
187
|
+
{!primaryCollapsed && (
|
|
188
|
+
<div>
|
|
189
|
+
<h1 className="text-[var(--color-sidebar-text)] font-bold text-lg">WiseBook</h1>
|
|
190
|
+
<p className="text-[var(--color-sidebar-text-secondary)] text-xs">ERP Next-Gen</p>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => setPrimaryCollapsed(!primaryCollapsed)}
|
|
196
|
+
className="text-[var(--color-sidebar-text-secondary)] hover:text-[var(--color-sidebar-text)] transition-colors"
|
|
197
|
+
aria-label={primaryCollapsed ? 'Développer le menu' : 'Réduire le menu'}
|
|
198
|
+
aria-expanded={!primaryCollapsed}
|
|
199
|
+
>
|
|
200
|
+
<ChevronLeft className={cn(
|
|
201
|
+
"w-5 h-5 transition-transform",
|
|
202
|
+
primaryCollapsed && "rotate-180"
|
|
203
|
+
)} />
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Primary Navigation */}
|
|
208
|
+
<nav
|
|
209
|
+
className="flex-1 py-4 overflow-y-auto"
|
|
210
|
+
role="menubar"
|
|
211
|
+
aria-label="Modules principaux"
|
|
212
|
+
>
|
|
213
|
+
{primaryMenuItems.map((item) => (
|
|
214
|
+
<button
|
|
215
|
+
key={item.id}
|
|
216
|
+
onClick={() => {
|
|
217
|
+
if (item.path) {
|
|
218
|
+
navigate(item.path);
|
|
219
|
+
} else {
|
|
220
|
+
setSelectedModule(item.id);
|
|
221
|
+
}
|
|
222
|
+
}}
|
|
223
|
+
className={cn(
|
|
224
|
+
'w-full flex items-center gap-3 px-4 py-3 transition-all duration-200',
|
|
225
|
+
'hover:bg-[var(--color-sidebar-hover)] relative group',
|
|
226
|
+
isModuleActive(item.id) && 'bg-[var(--color-sidebar-active)] border-l-4 border-[var(--color-primary)]',
|
|
227
|
+
primaryCollapsed && 'justify-center'
|
|
228
|
+
)}
|
|
229
|
+
role="menuitem"
|
|
230
|
+
aria-label={item.ariaLabel || item.label}
|
|
231
|
+
aria-current={isModuleActive(item.id) ? 'page' : undefined}
|
|
232
|
+
>
|
|
233
|
+
<div className={cn(
|
|
234
|
+
'transition-colors',
|
|
235
|
+
isModuleActive(item.id) ? 'text-[var(--color-primary)]' : 'text-[var(--color-sidebar-text-secondary)] group-hover:text-[var(--color-sidebar-text)]'
|
|
236
|
+
)}>
|
|
237
|
+
{item.icon}
|
|
238
|
+
</div>
|
|
239
|
+
{!primaryCollapsed && (
|
|
240
|
+
<>
|
|
241
|
+
<span className={cn(
|
|
242
|
+
'flex-1 text-left text-sm font-medium transition-colors',
|
|
243
|
+
isModuleActive(item.id) ? 'text-[var(--color-sidebar-text)]' : 'text-[var(--color-sidebar-text-secondary)] group-hover:text-[var(--color-sidebar-text)]'
|
|
244
|
+
)}>
|
|
245
|
+
{item.label}
|
|
246
|
+
</span>
|
|
247
|
+
{item.badge && (
|
|
248
|
+
<span className="px-2 py-0.5 text-xs bg-[var(--color-primary)] text-[var(--color-background)] rounded-full">
|
|
249
|
+
{item.badge}
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
254
|
+
{primaryCollapsed && (
|
|
255
|
+
<div className="absolute left-full ml-2 px-2 py-1 bg-[var(--color-sidebar-active)] text-[var(--color-sidebar-text)] text-xs rounded opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap z-50">
|
|
256
|
+
{item.label}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</button>
|
|
260
|
+
))}
|
|
261
|
+
</nav>
|
|
262
|
+
|
|
263
|
+
{/* User Section */}
|
|
264
|
+
<div className="p-4 border-t border-[var(--color-sidebar-border)]">
|
|
265
|
+
<div className={cn(
|
|
266
|
+
'flex items-center gap-3',
|
|
267
|
+
primaryCollapsed && 'justify-center'
|
|
268
|
+
)}>
|
|
269
|
+
<div className="w-10 h-10 bg-[var(--color-sidebar-avatar-bg)] rounded-full flex items-center justify-center">
|
|
270
|
+
<User className="w-5 h-5 text-[var(--color-sidebar-text-secondary)]" />
|
|
271
|
+
</div>
|
|
272
|
+
{!primaryCollapsed && (
|
|
273
|
+
<div className="flex-1">
|
|
274
|
+
<p className="text-sm font-medium text-[var(--color-sidebar-text)]">Admin</p>
|
|
275
|
+
<p className="text-xs text-[var(--color-sidebar-text-secondary)]">admin@wisebook.com</p>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</aside>
|
|
281
|
+
|
|
282
|
+
{/* Secondary Sidebar */}
|
|
283
|
+
{secondaryMenuItems[selectedModule] && (
|
|
284
|
+
<>
|
|
285
|
+
{/* Toggle button when collapsed */}
|
|
286
|
+
{secondaryCollapsed && (
|
|
287
|
+
<button
|
|
288
|
+
onClick={() => setSecondaryCollapsed(false)}
|
|
289
|
+
className="hidden lg:flex items-center justify-center w-12 h-full bg-[var(--color-background)] border-r border-[var(--color-border)] hover:bg-[var(--color-surface-hover)] transition-colors"
|
|
290
|
+
aria-label="Ouvrir le sous-menu"
|
|
291
|
+
>
|
|
292
|
+
<ChevronRight className="w-5 h-5 text-[var(--color-text-tertiary)]" />
|
|
293
|
+
</button>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
<aside
|
|
297
|
+
className={cn(
|
|
298
|
+
'hidden lg:flex flex-col bg-[var(--color-background)] border-r border-[var(--color-border)] transition-all duration-300',
|
|
299
|
+
secondaryCollapsed ? 'w-0 overflow-hidden' : 'w-64'
|
|
300
|
+
)}
|
|
301
|
+
role="navigation"
|
|
302
|
+
aria-label="Navigation secondaire"
|
|
303
|
+
>
|
|
304
|
+
<div className="h-16 flex items-center justify-between px-4 border-b border-[var(--color-border)]">
|
|
305
|
+
<h2 className="text-sm font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider whitespace-nowrap">
|
|
306
|
+
{primaryMenuItems.find(item => item.id === selectedModule)?.label}
|
|
307
|
+
</h2>
|
|
308
|
+
<button
|
|
309
|
+
onClick={() => setSecondaryCollapsed(!secondaryCollapsed)}
|
|
310
|
+
className="text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] flex-shrink-0"
|
|
311
|
+
aria-label={secondaryCollapsed ? 'Développer le sous-menu' : 'Réduire le sous-menu'}
|
|
312
|
+
>
|
|
313
|
+
<ChevronLeft className={cn(
|
|
314
|
+
"w-4 h-4 transition-transform",
|
|
315
|
+
secondaryCollapsed && "rotate-180"
|
|
316
|
+
)} />
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<nav
|
|
321
|
+
className="flex-1 py-4 overflow-y-auto"
|
|
322
|
+
role="menu"
|
|
323
|
+
aria-label="Sous-navigation"
|
|
324
|
+
>
|
|
325
|
+
{secondaryMenuItems[selectedModule]?.map((item) => (
|
|
326
|
+
<button
|
|
327
|
+
key={item.id}
|
|
328
|
+
onClick={() => item.path && navigate(item.path)}
|
|
329
|
+
className={cn(
|
|
330
|
+
'w-full flex items-center gap-3 px-4 py-2.5 transition-all duration-200',
|
|
331
|
+
'hover:bg-[var(--color-surface-hover)]',
|
|
332
|
+
isActive(item.path || '') && 'bg-[var(--color-primary-light)] border-l-4 border-[var(--color-primary)]'
|
|
333
|
+
)}
|
|
334
|
+
role="menuitem"
|
|
335
|
+
aria-current={isActive(item.path || '') ? 'page' : undefined}
|
|
336
|
+
>
|
|
337
|
+
{item.icon && (
|
|
338
|
+
<div className={cn(
|
|
339
|
+
'transition-colors',
|
|
340
|
+
isActive(item.path || '') ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-tertiary)]'
|
|
341
|
+
)}>
|
|
342
|
+
{item.icon}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
<span className={cn(
|
|
346
|
+
'flex-1 text-left text-sm',
|
|
347
|
+
isActive(item.path || '') ? 'text-[var(--color-primary)] font-medium' : 'text-[var(--color-text-secondary)]'
|
|
348
|
+
)}>
|
|
349
|
+
{item.label}
|
|
350
|
+
</span>
|
|
351
|
+
{item.badge && (
|
|
352
|
+
<span className="px-2 py-0.5 text-xs bg-[var(--color-primary)] text-white rounded-full">
|
|
353
|
+
{item.badge}
|
|
354
|
+
</span>
|
|
355
|
+
)}
|
|
356
|
+
</button>
|
|
357
|
+
))}
|
|
358
|
+
</nav>
|
|
359
|
+
</aside>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{/* Mobile Sidebar Overlay */}
|
|
364
|
+
{mobileMenuOpen && (
|
|
365
|
+
<div
|
|
366
|
+
className="fixed inset-0 bg-black bg-opacity-50 z-50 lg:hidden"
|
|
367
|
+
onClick={() => setMobileMenuOpen(false)}
|
|
368
|
+
aria-hidden="true"
|
|
369
|
+
>
|
|
370
|
+
<aside
|
|
371
|
+
className="w-80 h-full bg-[var(--color-sidebar-bg)]"
|
|
372
|
+
onClick={(e) => e.stopPropagation()}
|
|
373
|
+
role="navigation"
|
|
374
|
+
aria-label="Navigation mobile"
|
|
375
|
+
>
|
|
376
|
+
<div className="h-16 flex items-center justify-between px-4 border-b border-[var(--color-sidebar-border)]">
|
|
377
|
+
<div className="flex items-center gap-3">
|
|
378
|
+
<div className="w-10 h-10 bg-[var(--color-primary)] rounded-lg flex items-center justify-center">
|
|
379
|
+
<span className="text-[var(--color-background)] font-bold text-xl">W</span>
|
|
380
|
+
</div>
|
|
381
|
+
<div>
|
|
382
|
+
<h1 className="text-white font-bold text-lg">WiseBook</h1>
|
|
383
|
+
<p className="text-gray-400 text-xs">ERP Next-Gen</p>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
<button
|
|
387
|
+
onClick={() => setMobileMenuOpen(false)}
|
|
388
|
+
className="text-[var(--color-sidebar-text-secondary)]"
|
|
389
|
+
aria-label="Fermer le menu"
|
|
390
|
+
>
|
|
391
|
+
<X className="w-6 h-6" />
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
<nav className="py-4" role="menubar">
|
|
395
|
+
{primaryMenuItems.map((item) => (
|
|
396
|
+
<div key={item.id}>
|
|
397
|
+
<button
|
|
398
|
+
onClick={() => {
|
|
399
|
+
if (item.path) {
|
|
400
|
+
navigate(item.path);
|
|
401
|
+
setMobileMenuOpen(false);
|
|
402
|
+
} else {
|
|
403
|
+
setSelectedModule(item.id);
|
|
404
|
+
}
|
|
405
|
+
}}
|
|
406
|
+
className={cn(
|
|
407
|
+
'w-full flex items-center gap-3 px-4 py-3 transition-all duration-200',
|
|
408
|
+
'hover:bg-[var(--color-sidebar-hover)]',
|
|
409
|
+
isModuleActive(item.id) && 'bg-[var(--color-sidebar-active)] border-l-4 border-[var(--color-primary)]'
|
|
410
|
+
)}
|
|
411
|
+
role="menuitem"
|
|
412
|
+
aria-current={isModuleActive(item.id) ? 'page' : undefined}
|
|
413
|
+
>
|
|
414
|
+
<div className={cn(
|
|
415
|
+
'transition-colors',
|
|
416
|
+
isModuleActive(item.id) ? 'text-[var(--color-primary)]' : 'text-[var(--color-sidebar-text-secondary)]'
|
|
417
|
+
)}>
|
|
418
|
+
{item.icon}
|
|
419
|
+
</div>
|
|
420
|
+
<span className={cn(
|
|
421
|
+
'flex-1 text-left text-sm font-medium',
|
|
422
|
+
isModuleActive(item.id) ? 'text-[var(--color-sidebar-text)]' : 'text-[var(--color-sidebar-text-secondary)]'
|
|
423
|
+
)}>
|
|
424
|
+
{item.label}
|
|
425
|
+
</span>
|
|
426
|
+
{item.badge && (
|
|
427
|
+
<span className="px-2 py-0.5 text-xs bg-[var(--color-primary)] text-[var(--color-background)] rounded-full">
|
|
428
|
+
{item.badge}
|
|
429
|
+
</span>
|
|
430
|
+
)}
|
|
431
|
+
</button>
|
|
432
|
+
|
|
433
|
+
{/* Mobile Submenu */}
|
|
434
|
+
{isModuleActive(item.id) && secondaryMenuItems[item.id] && (
|
|
435
|
+
<div className="bg-[var(--color-sidebar-submenu-bg)] py-2">
|
|
436
|
+
{secondaryMenuItems[item.id].map((subItem) => (
|
|
437
|
+
<button
|
|
438
|
+
key={subItem.id}
|
|
439
|
+
onClick={() => {
|
|
440
|
+
if (subItem.path) {
|
|
441
|
+
navigate(subItem.path);
|
|
442
|
+
setMobileMenuOpen(false);
|
|
443
|
+
}
|
|
444
|
+
}}
|
|
445
|
+
className={cn(
|
|
446
|
+
'w-full flex items-center gap-3 pl-12 pr-4 py-2 text-sm',
|
|
447
|
+
'hover:bg-[var(--color-sidebar-hover)]',
|
|
448
|
+
isActive(subItem.path || '') && 'bg-[var(--color-sidebar-active)] text-[var(--color-primary)]'
|
|
449
|
+
)}
|
|
450
|
+
>
|
|
451
|
+
{subItem.icon}
|
|
452
|
+
<span className={cn(
|
|
453
|
+
isActive(subItem.path || '') ? 'text-[var(--color-primary)]' : 'text-[var(--color-sidebar-text-secondary)]'
|
|
454
|
+
)}>
|
|
455
|
+
{subItem.label}
|
|
456
|
+
</span>
|
|
457
|
+
</button>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
</div>
|
|
462
|
+
))}
|
|
463
|
+
</nav>
|
|
464
|
+
</aside>
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
{/* Main Content Area */}
|
|
469
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
470
|
+
{/* Header */}
|
|
471
|
+
<header
|
|
472
|
+
className="h-14 bg-[var(--color-background)] border-b border-[var(--color-border)] flex items-center justify-between px-3 lg:px-4"
|
|
473
|
+
role="banner"
|
|
474
|
+
>
|
|
475
|
+
<div className="flex items-center gap-4 flex-1">
|
|
476
|
+
<button
|
|
477
|
+
onClick={() => setMobileMenuOpen(true)}
|
|
478
|
+
className="lg:hidden text-[var(--color-text-primary)]"
|
|
479
|
+
aria-label="Ouvrir le menu mobile"
|
|
480
|
+
>
|
|
481
|
+
<Menu className="w-6 h-6" />
|
|
482
|
+
</button>
|
|
483
|
+
|
|
484
|
+
{/* Breadcrumbs */}
|
|
485
|
+
<nav
|
|
486
|
+
className="hidden sm:flex items-center gap-2 text-sm"
|
|
487
|
+
aria-label="Fil d'Ariane"
|
|
488
|
+
>
|
|
489
|
+
{getBreadcrumbs().map((crumb, index) => (
|
|
490
|
+
<React.Fragment key={crumb.path}>
|
|
491
|
+
{index > 0 && <ChevronRight className="w-4 h-4 text-[var(--color-text-tertiary)]" />}
|
|
492
|
+
<button
|
|
493
|
+
onClick={() => navigate(crumb.path)}
|
|
494
|
+
className={cn(
|
|
495
|
+
'hover:text-[var(--color-primary)]',
|
|
496
|
+
index === getBreadcrumbs().length - 1
|
|
497
|
+
? 'text-[var(--color-text-primary)] font-medium'
|
|
498
|
+
: 'text-[var(--color-text-tertiary)]'
|
|
499
|
+
)}
|
|
500
|
+
>
|
|
501
|
+
{crumb.label}
|
|
502
|
+
</button>
|
|
503
|
+
</React.Fragment>
|
|
504
|
+
))}
|
|
505
|
+
</nav>
|
|
506
|
+
|
|
507
|
+
{/* Search */}
|
|
508
|
+
<div className="relative max-w-md flex-1 hidden lg:block">
|
|
509
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-tertiary)] w-5 h-5" />
|
|
510
|
+
<input
|
|
511
|
+
id="global-search"
|
|
512
|
+
type="text"
|
|
513
|
+
placeholder="Rechercher... (Alt+S)"
|
|
514
|
+
value={searchQuery}
|
|
515
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
516
|
+
className="w-full pl-10 pr-4 py-2 bg-[var(--color-surface-hover)] border border-[var(--color-border)] rounded-lg text-sm focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary-light)]"
|
|
517
|
+
aria-label="Recherche globale"
|
|
518
|
+
/>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<div className="flex items-center gap-3">
|
|
523
|
+
{/* Theme Selector */}
|
|
524
|
+
<div className="relative">
|
|
525
|
+
<button
|
|
526
|
+
onClick={() => setShowThemeMenu(!showThemeMenu)}
|
|
527
|
+
className="p-2 hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors"
|
|
528
|
+
title="Changer le thème"
|
|
529
|
+
aria-label="Sélecteur de thème"
|
|
530
|
+
aria-expanded={showThemeMenu}
|
|
531
|
+
>
|
|
532
|
+
<Palette className="w-5 h-5 text-[var(--color-text-secondary)]" />
|
|
533
|
+
</button>
|
|
534
|
+
{showThemeMenu && (
|
|
535
|
+
<div
|
|
536
|
+
className="absolute right-0 mt-2 w-64 bg-[var(--color-background)] rounded-lg shadow-xl border border-[var(--color-border)] z-50"
|
|
537
|
+
role="menu"
|
|
538
|
+
aria-label="Sélection du thème"
|
|
539
|
+
>
|
|
540
|
+
<div className="p-2">
|
|
541
|
+
<p className="px-3 py-2 text-xs font-semibold text-[var(--color-text-tertiary)] uppercase">
|
|
542
|
+
Thèmes disponibles
|
|
543
|
+
</p>
|
|
544
|
+
<button
|
|
545
|
+
onClick={() => handleThemeChange('elegant')}
|
|
546
|
+
className={cn(
|
|
547
|
+
'w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors',
|
|
548
|
+
themeType === 'elegant' && 'bg-[var(--color-primary-light)]'
|
|
549
|
+
)}
|
|
550
|
+
role="menuitem"
|
|
551
|
+
>
|
|
552
|
+
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-accent)]" />
|
|
553
|
+
<div className="text-left">
|
|
554
|
+
<p className="text-sm font-medium">Élégance Sobre</p>
|
|
555
|
+
<p className="text-xs text-[var(--color-text-tertiary)]">Finance traditionnelle</p>
|
|
556
|
+
</div>
|
|
557
|
+
</button>
|
|
558
|
+
<button
|
|
559
|
+
onClick={() => handleThemeChange('fintech')}
|
|
560
|
+
className={cn(
|
|
561
|
+
'w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors',
|
|
562
|
+
themeType === 'fintech' && 'bg-[var(--color-primary-light)]'
|
|
563
|
+
)}
|
|
564
|
+
role="menuitem"
|
|
565
|
+
>
|
|
566
|
+
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[var(--color-success)] to-[var(--color-text-primary)]" />
|
|
567
|
+
<div className="text-left">
|
|
568
|
+
<p className="text-sm font-medium">Modern Fintech</p>
|
|
569
|
+
<p className="text-xs text-[var(--color-text-tertiary)]">Tableau de bord moderne</p>
|
|
570
|
+
</div>
|
|
571
|
+
</button>
|
|
572
|
+
<button
|
|
573
|
+
onClick={() => handleThemeChange('minimalist')}
|
|
574
|
+
className={cn(
|
|
575
|
+
'w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors',
|
|
576
|
+
themeType === 'minimalist' && 'bg-[var(--color-primary-light)]'
|
|
577
|
+
)}
|
|
578
|
+
role="menuitem"
|
|
579
|
+
>
|
|
580
|
+
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[var(--color-text-secondary)] to-[var(--color-accent)]" />
|
|
581
|
+
<div className="text-left">
|
|
582
|
+
<p className="text-sm font-medium">Minimaliste Premium</p>
|
|
583
|
+
<p className="text-xs text-[var(--color-text-tertiary)]">Élégance minimaliste</p>
|
|
584
|
+
</div>
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
{/* Currency Display */}
|
|
592
|
+
<div className="flex items-center px-3 py-1.5 bg-[var(--color-surface)] rounded-lg border border-[var(--color-border)]">
|
|
593
|
+
<DollarSign className="w-4 h-4 text-[var(--color-primary)] mr-2" />
|
|
594
|
+
<span className="text-sm font-medium text-[var(--color-text-primary)]">FCFA</span>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
{/* Notifications */}
|
|
598
|
+
<div className="relative">
|
|
599
|
+
<button
|
|
600
|
+
className="relative p-2 hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors"
|
|
601
|
+
onClick={() => setShowNotifications(!showNotifications)}
|
|
602
|
+
aria-label={`Notifications ${notifications.filter(n => !n.read).length > 0 ? `(${notifications.filter(n => !n.read).length} non lues)` : ''}`}
|
|
603
|
+
aria-expanded={showNotifications}
|
|
604
|
+
>
|
|
605
|
+
<Bell className="w-5 h-5 text-[var(--color-text-secondary)]" />
|
|
606
|
+
{notifications.filter(n => !n.read).length > 0 && (
|
|
607
|
+
<span className="absolute top-1 right-1 w-2 h-2 bg-[var(--color-error)] rounded-full"></span>
|
|
608
|
+
)}
|
|
609
|
+
</button>
|
|
610
|
+
|
|
611
|
+
{showNotifications && (
|
|
612
|
+
<div
|
|
613
|
+
className="absolute right-0 mt-2 w-80 bg-[var(--color-background)] rounded-lg shadow-xl border border-[var(--color-border)] z-50 max-h-96 overflow-y-auto"
|
|
614
|
+
role="region"
|
|
615
|
+
aria-label="Centre de notifications"
|
|
616
|
+
>
|
|
617
|
+
<div className="p-4 border-b border-[var(--color-border)]">
|
|
618
|
+
<h3 className="font-semibold text-[var(--color-text-primary)]">Notifications</h3>
|
|
619
|
+
</div>
|
|
620
|
+
<div className="divide-y divide-[var(--color-border)]">
|
|
621
|
+
{notifications.map((notif) => (
|
|
622
|
+
<div
|
|
623
|
+
key={notif.id}
|
|
624
|
+
className={cn(
|
|
625
|
+
"p-4 hover:bg-[var(--color-surface-hover)] cursor-pointer",
|
|
626
|
+
!notif.read && "bg-[var(--color-primary-light)] bg-opacity-10"
|
|
627
|
+
)}
|
|
628
|
+
onClick={() => markNotificationAsRead(notif.id)}
|
|
629
|
+
>
|
|
630
|
+
<div className="flex items-start gap-3">
|
|
631
|
+
<div className={cn(
|
|
632
|
+
"w-2 h-2 rounded-full mt-2",
|
|
633
|
+
notif.type === 'error' && "bg-[var(--color-error)]",
|
|
634
|
+
notif.type === 'warning' && "bg-[var(--color-warning)]",
|
|
635
|
+
notif.type === 'success' && "bg-[var(--color-success)]",
|
|
636
|
+
notif.type === 'info' && "bg-[var(--color-info)]"
|
|
637
|
+
)} />
|
|
638
|
+
<div className="flex-1">
|
|
639
|
+
<p className="text-sm font-medium text-[var(--color-text-primary)]">{notif.title}</p>
|
|
640
|
+
<p className="text-xs text-[var(--color-text-secondary)] mt-1">{notif.message}</p>
|
|
641
|
+
<p className="text-xs text-[var(--color-text-tertiary)] mt-2">
|
|
642
|
+
{notif.timestamp.toLocaleTimeString()}
|
|
643
|
+
</p>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
))}
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
{/* User Menu */}
|
|
654
|
+
<div className="relative">
|
|
655
|
+
<button
|
|
656
|
+
onClick={() => setShowUserMenu(!showUserMenu)}
|
|
657
|
+
className="flex items-center gap-2 p-2 hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors"
|
|
658
|
+
aria-label="Menu utilisateur"
|
|
659
|
+
aria-expanded={showUserMenu}
|
|
660
|
+
>
|
|
661
|
+
<div className="w-8 h-8 bg-[var(--color-primary)] rounded-full flex items-center justify-center">
|
|
662
|
+
<User className="w-4 h-4 text-[var(--color-background)]" />
|
|
663
|
+
</div>
|
|
664
|
+
</button>
|
|
665
|
+
{showUserMenu && (
|
|
666
|
+
<div
|
|
667
|
+
className="absolute right-0 mt-2 w-56 bg-[var(--color-background)] rounded-lg shadow-xl border border-[var(--color-border)] z-50"
|
|
668
|
+
role="menu"
|
|
669
|
+
aria-label="Menu utilisateur"
|
|
670
|
+
>
|
|
671
|
+
<div className="p-2">
|
|
672
|
+
<button
|
|
673
|
+
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors"
|
|
674
|
+
role="menuitem"
|
|
675
|
+
>
|
|
676
|
+
<User className="w-4 h-4" />
|
|
677
|
+
<span className="text-sm">Mon profil</span>
|
|
678
|
+
</button>
|
|
679
|
+
<button
|
|
680
|
+
onClick={() => {
|
|
681
|
+
navigate('/settings');
|
|
682
|
+
setShowUserMenu(false);
|
|
683
|
+
}}
|
|
684
|
+
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors"
|
|
685
|
+
role="menuitem"
|
|
686
|
+
>
|
|
687
|
+
<Settings className="w-4 h-4" />
|
|
688
|
+
<span className="text-sm">Paramètres</span>
|
|
689
|
+
</button>
|
|
690
|
+
<button
|
|
691
|
+
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors"
|
|
692
|
+
role="menuitem"
|
|
693
|
+
>
|
|
694
|
+
<HelpCircle className="w-4 h-4" />
|
|
695
|
+
<span className="text-sm">Aide</span>
|
|
696
|
+
</button>
|
|
697
|
+
<hr className="my-2 border-[var(--color-border)]" />
|
|
698
|
+
<button
|
|
699
|
+
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-error)] transition-colors"
|
|
700
|
+
role="menuitem"
|
|
701
|
+
>
|
|
702
|
+
<LogOut className="w-4 h-4" />
|
|
703
|
+
<span className="text-sm">Déconnexion</span>
|
|
704
|
+
</button>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
)}
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</header>
|
|
711
|
+
|
|
712
|
+
{/* Page Content */}
|
|
713
|
+
<main
|
|
714
|
+
id="main-content"
|
|
715
|
+
className="flex-1 overflow-y-auto bg-[var(--color-background)]"
|
|
716
|
+
role="main"
|
|
717
|
+
>
|
|
718
|
+
<div className="p-3 lg:p-4">
|
|
719
|
+
{children}
|
|
720
|
+
</div>
|
|
721
|
+
</main>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
);
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
export default RewiseLayout;
|