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,382 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Bell,
|
|
4
|
+
Shield,
|
|
5
|
+
Palette,
|
|
6
|
+
Globe,
|
|
7
|
+
Monitor,
|
|
8
|
+
Moon,
|
|
9
|
+
Sun,
|
|
10
|
+
Eye,
|
|
11
|
+
Lock,
|
|
12
|
+
Smartphone,
|
|
13
|
+
Key,
|
|
14
|
+
Trash2,
|
|
15
|
+
Save,
|
|
16
|
+
AlertTriangle
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { useToast } from '../contexts/ToastContext';
|
|
19
|
+
|
|
20
|
+
const Settings: React.FC = () => {
|
|
21
|
+
const [activeTab, setActiveTab] = useState('general');
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const { success, error: showError, warning } = useToast();
|
|
24
|
+
|
|
25
|
+
const [settings, setSettings] = useState({
|
|
26
|
+
// General Settings
|
|
27
|
+
language: 'fr',
|
|
28
|
+
timezone: 'Europe/Paris',
|
|
29
|
+
theme: 'light',
|
|
30
|
+
|
|
31
|
+
// Notifications
|
|
32
|
+
emailNotifications: true,
|
|
33
|
+
pushNotifications: true,
|
|
34
|
+
weeklyReport: true,
|
|
35
|
+
taskReminders: true,
|
|
36
|
+
teamUpdates: false,
|
|
37
|
+
|
|
38
|
+
// Privacy & Security
|
|
39
|
+
twoFactorAuth: false,
|
|
40
|
+
profileVisibility: 'team',
|
|
41
|
+
activityStatus: true,
|
|
42
|
+
dataSharing: false,
|
|
43
|
+
|
|
44
|
+
// Appearance
|
|
45
|
+
sidebarCollapsed: false,
|
|
46
|
+
compactMode: false,
|
|
47
|
+
animations: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const handleSettingChange = (key: string, value: any) => {
|
|
51
|
+
setSettings(prev => ({ ...prev, [key]: value }));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleSave = async () => {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
try {
|
|
57
|
+
// Simuler un appel API
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
59
|
+
success('Paramètres sauvegardés avec succès !');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
showError('Erreur lors de la sauvegarde des paramètres');
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleResetPassword = () => {
|
|
68
|
+
warning('Un email de réinitialisation sera envoyé');
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleDeleteAccount = () => {
|
|
72
|
+
showError('Fonctionnalité de suppression de compte non disponible');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const tabs = [
|
|
76
|
+
{ id: 'general', label: 'Général', icon: Monitor },
|
|
77
|
+
{ id: 'notifications', label: 'Notifications', icon: Bell },
|
|
78
|
+
{ id: 'security', label: 'Sécurité', icon: Shield },
|
|
79
|
+
{ id: 'appearance', label: 'Apparence', icon: Palette },
|
|
80
|
+
{ id: 'privacy', label: 'Confidentialité', icon: Eye }
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const ToggleSwitch = ({ enabled, onChange, label, description }: {
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
onChange: (value: boolean) => void;
|
|
86
|
+
label: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
}) => (
|
|
89
|
+
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
|
90
|
+
<div>
|
|
91
|
+
<h4 className="font-medium text-gray-900">{label}</h4>
|
|
92
|
+
{description && <p className="text-sm text-gray-600">{description}</p>}
|
|
93
|
+
</div>
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => onChange(!enabled)}
|
|
96
|
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
97
|
+
enabled ? 'bg-[#8290A9]' : 'bg-gray-300'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<span
|
|
101
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
102
|
+
enabled ? 'translate-x-6' : 'translate-x-1'
|
|
103
|
+
}`}
|
|
104
|
+
/>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const SelectField = ({ value, onChange, options, label }: {
|
|
110
|
+
value: string;
|
|
111
|
+
onChange: (value: string) => void;
|
|
112
|
+
options: { value: string; label: string }[];
|
|
113
|
+
label: string;
|
|
114
|
+
}) => (
|
|
115
|
+
<div>
|
|
116
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">{label}</label>
|
|
117
|
+
<select
|
|
118
|
+
value={value}
|
|
119
|
+
onChange={(e) => onChange(e.target.value)}
|
|
120
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
121
|
+
>
|
|
122
|
+
{options.map((option) => (
|
|
123
|
+
<option key={option.value} value={option.value}>
|
|
124
|
+
{option.label}
|
|
125
|
+
</option>
|
|
126
|
+
))}
|
|
127
|
+
</select>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const renderTabContent = () => {
|
|
132
|
+
switch (activeTab) {
|
|
133
|
+
case 'general':
|
|
134
|
+
return (
|
|
135
|
+
<div className="space-y-6">
|
|
136
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
137
|
+
<SelectField
|
|
138
|
+
label="Langue"
|
|
139
|
+
value={settings.language}
|
|
140
|
+
onChange={(value) => handleSettingChange('language', value)}
|
|
141
|
+
options={[
|
|
142
|
+
{ value: 'fr', label: 'Français' },
|
|
143
|
+
{ value: 'en', label: 'English' },
|
|
144
|
+
{ value: 'es', label: 'Español' }
|
|
145
|
+
]}
|
|
146
|
+
/>
|
|
147
|
+
<SelectField
|
|
148
|
+
label="Fuseau horaire"
|
|
149
|
+
value={settings.timezone}
|
|
150
|
+
onChange={(value) => handleSettingChange('timezone', value)}
|
|
151
|
+
options={[
|
|
152
|
+
{ value: 'Europe/Paris', label: 'Paris (UTC+1)' },
|
|
153
|
+
{ value: 'Europe/London', label: 'Londres (UTC+0)' },
|
|
154
|
+
{ value: 'America/New_York', label: 'New York (UTC-5)' }
|
|
155
|
+
]}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
case 'notifications':
|
|
162
|
+
return (
|
|
163
|
+
<div className="space-y-4">
|
|
164
|
+
<ToggleSwitch
|
|
165
|
+
enabled={settings.emailNotifications}
|
|
166
|
+
onChange={(value) => handleSettingChange('emailNotifications', value)}
|
|
167
|
+
label="Notifications par email"
|
|
168
|
+
description="Recevoir des notifications importantes par email"
|
|
169
|
+
/>
|
|
170
|
+
<ToggleSwitch
|
|
171
|
+
enabled={settings.pushNotifications}
|
|
172
|
+
onChange={(value) => handleSettingChange('pushNotifications', value)}
|
|
173
|
+
label="Notifications push"
|
|
174
|
+
description="Recevoir des notifications en temps réel"
|
|
175
|
+
/>
|
|
176
|
+
<ToggleSwitch
|
|
177
|
+
enabled={settings.weeklyReport}
|
|
178
|
+
onChange={(value) => handleSettingChange('weeklyReport', value)}
|
|
179
|
+
label="Rapport hebdomadaire"
|
|
180
|
+
description="Recevoir un résumé de votre activité chaque semaine"
|
|
181
|
+
/>
|
|
182
|
+
<ToggleSwitch
|
|
183
|
+
enabled={settings.taskReminders}
|
|
184
|
+
onChange={(value) => handleSettingChange('taskReminders', value)}
|
|
185
|
+
label="Rappels de tâches"
|
|
186
|
+
description="Recevoir des rappels pour les tâches en retard"
|
|
187
|
+
/>
|
|
188
|
+
<ToggleSwitch
|
|
189
|
+
enabled={settings.teamUpdates}
|
|
190
|
+
onChange={(value) => handleSettingChange('teamUpdates', value)}
|
|
191
|
+
label="Mises à jour d'équipe"
|
|
192
|
+
description="Recevoir des notifications sur l'activité de l'équipe"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
case 'security':
|
|
198
|
+
return (
|
|
199
|
+
<div className="space-y-6">
|
|
200
|
+
<ToggleSwitch
|
|
201
|
+
enabled={settings.twoFactorAuth}
|
|
202
|
+
onChange={(value) => handleSettingChange('twoFactorAuth', value)}
|
|
203
|
+
label="Authentification à deux facteurs"
|
|
204
|
+
description="Ajouter une couche de sécurité supplémentaire"
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
|
|
208
|
+
<h4 className="font-medium text-gray-900 flex items-center space-x-2">
|
|
209
|
+
<Key className="w-4 h-4" />
|
|
210
|
+
<span>Gestion du mot de passe</span>
|
|
211
|
+
</h4>
|
|
212
|
+
<button
|
|
213
|
+
onClick={handleResetPassword}
|
|
214
|
+
className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
|
|
215
|
+
>
|
|
216
|
+
Réinitialiser le mot de passe
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
221
|
+
<h4 className="font-medium text-red-900 flex items-center space-x-2 mb-2">
|
|
222
|
+
<AlertTriangle className="w-4 h-4" />
|
|
223
|
+
<span>Zone de danger</span>
|
|
224
|
+
</h4>
|
|
225
|
+
<p className="text-sm text-red-700 mb-4">
|
|
226
|
+
Cette action est irréversible et supprimera définitivement votre compte.
|
|
227
|
+
</p>
|
|
228
|
+
<button
|
|
229
|
+
onClick={handleDeleteAccount}
|
|
230
|
+
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2"
|
|
231
|
+
>
|
|
232
|
+
<Trash2 className="w-4 h-4" />
|
|
233
|
+
<span>Supprimer le compte</span>
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
case 'appearance':
|
|
240
|
+
return (
|
|
241
|
+
<div className="space-y-6">
|
|
242
|
+
<div>
|
|
243
|
+
<label className="block text-sm font-medium text-gray-700 mb-3">Thème</label>
|
|
244
|
+
<div className="grid grid-cols-3 gap-4">
|
|
245
|
+
{[
|
|
246
|
+
{ value: 'light', label: 'Clair', icon: Sun },
|
|
247
|
+
{ value: 'dark', label: 'Sombre', icon: Moon },
|
|
248
|
+
{ value: 'auto', label: 'Auto', icon: Monitor }
|
|
249
|
+
].map((theme) => {
|
|
250
|
+
const Icon = theme.icon;
|
|
251
|
+
return (
|
|
252
|
+
<button
|
|
253
|
+
key={theme.value}
|
|
254
|
+
onClick={() => handleSettingChange('theme', theme.value)}
|
|
255
|
+
className={`p-4 border-2 rounded-lg transition-colors ${
|
|
256
|
+
settings.theme === theme.value
|
|
257
|
+
? 'border-[#8290A9] bg-blue-50'
|
|
258
|
+
: 'border-gray-200 hover:border-gray-300'
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
261
|
+
<Icon className="w-6 h-6 mx-auto mb-2 text-gray-600" />
|
|
262
|
+
<span className="text-sm font-medium">{theme.label}</span>
|
|
263
|
+
</button>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<ToggleSwitch
|
|
270
|
+
enabled={settings.sidebarCollapsed}
|
|
271
|
+
onChange={(value) => handleSettingChange('sidebarCollapsed', value)}
|
|
272
|
+
label="Sidebar réduite par défaut"
|
|
273
|
+
description="Commencer avec la sidebar réduite"
|
|
274
|
+
/>
|
|
275
|
+
|
|
276
|
+
<ToggleSwitch
|
|
277
|
+
enabled={settings.compactMode}
|
|
278
|
+
onChange={(value) => handleSettingChange('compactMode', value)}
|
|
279
|
+
label="Mode compact"
|
|
280
|
+
description="Affichage plus dense pour économiser l'espace"
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
<ToggleSwitch
|
|
284
|
+
enabled={settings.animations}
|
|
285
|
+
onChange={(value) => handleSettingChange('animations', value)}
|
|
286
|
+
label="Animations"
|
|
287
|
+
description="Activer les animations et transitions"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
case 'privacy':
|
|
293
|
+
return (
|
|
294
|
+
<div className="space-y-6">
|
|
295
|
+
<SelectField
|
|
296
|
+
label="Visibilité du profil"
|
|
297
|
+
value={settings.profileVisibility}
|
|
298
|
+
onChange={(value) => handleSettingChange('profileVisibility', value)}
|
|
299
|
+
options={[
|
|
300
|
+
{ value: 'public', label: 'Public' },
|
|
301
|
+
{ value: 'team', label: 'Équipe uniquement' },
|
|
302
|
+
{ value: 'private', label: 'Privé' }
|
|
303
|
+
]}
|
|
304
|
+
/>
|
|
305
|
+
|
|
306
|
+
<ToggleSwitch
|
|
307
|
+
enabled={settings.activityStatus}
|
|
308
|
+
onChange={(value) => handleSettingChange('activityStatus', value)}
|
|
309
|
+
label="Statut d'activité"
|
|
310
|
+
description="Permettre aux autres de voir quand vous êtes en ligne"
|
|
311
|
+
/>
|
|
312
|
+
|
|
313
|
+
<ToggleSwitch
|
|
314
|
+
enabled={settings.dataSharing}
|
|
315
|
+
onChange={(value) => handleSettingChange('dataSharing', value)}
|
|
316
|
+
label="Partage de données analytiques"
|
|
317
|
+
description="Partager des données anonymisées pour améliorer le service"
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
default:
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<div className="space-y-6">
|
|
329
|
+
{/* Header */}
|
|
330
|
+
<div className="flex items-center justify-between">
|
|
331
|
+
<div>
|
|
332
|
+
<h1 className="text-3xl font-bold text-gray-900">Paramètres</h1>
|
|
333
|
+
<p className="text-gray-600 mt-2">
|
|
334
|
+
Gérez vos préférences et la configuration de votre compte
|
|
335
|
+
</p>
|
|
336
|
+
</div>
|
|
337
|
+
<button
|
|
338
|
+
onClick={handleSave}
|
|
339
|
+
disabled={loading}
|
|
340
|
+
className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors flex items-center space-x-2 disabled:opacity-50"
|
|
341
|
+
>
|
|
342
|
+
<Save className="w-4 h-4" />
|
|
343
|
+
<span>{loading ? 'Sauvegarde...' : 'Sauvegarder'}</span>
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div className="flex flex-col lg:flex-row gap-6">
|
|
348
|
+
{/* Sidebar */}
|
|
349
|
+
<div className="lg:w-64">
|
|
350
|
+
<nav className="space-y-2">
|
|
351
|
+
{tabs.map((tab) => {
|
|
352
|
+
const Icon = tab.icon;
|
|
353
|
+
return (
|
|
354
|
+
<button
|
|
355
|
+
key={tab.id}
|
|
356
|
+
onClick={() => setActiveTab(tab.id)}
|
|
357
|
+
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
|
358
|
+
activeTab === tab.id
|
|
359
|
+
? 'bg-[#8290A9] text-white'
|
|
360
|
+
: 'text-gray-600 hover:bg-gray-100'
|
|
361
|
+
}`}
|
|
362
|
+
>
|
|
363
|
+
<Icon className="w-5 h-5" />
|
|
364
|
+
<span className="font-medium">{tab.label}</span>
|
|
365
|
+
</button>
|
|
366
|
+
);
|
|
367
|
+
})}
|
|
368
|
+
</nav>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Content */}
|
|
372
|
+
<div className="flex-1">
|
|
373
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
374
|
+
{renderTabContent()}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export default Settings;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Plus, Search, Filter, MoreVertical, Mail, Phone, MapPin } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
const Team: React.FC = () => {
|
|
5
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
6
|
+
|
|
7
|
+
const teamMembers = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
name: 'Alice Martin',
|
|
11
|
+
role: 'Lead Developer',
|
|
12
|
+
email: 'alice.martin@rewise.com',
|
|
13
|
+
phone: '+33 6 12 34 56 78',
|
|
14
|
+
location: 'Paris, France',
|
|
15
|
+
avatar: null,
|
|
16
|
+
status: 'online',
|
|
17
|
+
projects: 3
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 2,
|
|
21
|
+
name: 'Thomas Dubois',
|
|
22
|
+
role: 'UI/UX Designer',
|
|
23
|
+
email: 'thomas.dubois@rewise.com',
|
|
24
|
+
phone: '+33 6 87 65 43 21',
|
|
25
|
+
location: 'Lyon, France',
|
|
26
|
+
avatar: null,
|
|
27
|
+
status: 'away',
|
|
28
|
+
projects: 2
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 3,
|
|
32
|
+
name: 'Sophie Laurent',
|
|
33
|
+
role: 'Project Manager',
|
|
34
|
+
email: 'sophie.laurent@rewise.com',
|
|
35
|
+
phone: '+33 6 11 22 33 44',
|
|
36
|
+
location: 'Marseille, France',
|
|
37
|
+
avatar: null,
|
|
38
|
+
status: 'online',
|
|
39
|
+
projects: 5
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 4,
|
|
43
|
+
name: 'Pierre Moreau',
|
|
44
|
+
role: 'Backend Developer',
|
|
45
|
+
email: 'pierre.moreau@rewise.com',
|
|
46
|
+
phone: '+33 6 55 66 77 88',
|
|
47
|
+
location: 'Toulouse, France',
|
|
48
|
+
avatar: null,
|
|
49
|
+
status: 'offline',
|
|
50
|
+
projects: 2
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const filteredMembers = teamMembers.filter(member =>
|
|
55
|
+
member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
56
|
+
member.role.toLowerCase().includes(searchTerm.toLowerCase())
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const getStatusColor = (status: string) => {
|
|
60
|
+
switch (status) {
|
|
61
|
+
case 'online': return 'bg-green-500';
|
|
62
|
+
case 'away': return 'bg-yellow-500';
|
|
63
|
+
case 'offline': return 'bg-gray-400';
|
|
64
|
+
default: return 'bg-gray-400';
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getInitials = (name: string) => {
|
|
69
|
+
return name.split(' ').map(n => n[0]).join('').toUpperCase();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-6">
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div>
|
|
77
|
+
<h1 className="text-3xl font-bold text-gray-900">Équipe</h1>
|
|
78
|
+
<p className="text-gray-600 mt-2">
|
|
79
|
+
Gérez votre équipe et suivez les performances de chaque membre
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<button className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors flex items-center space-x-2">
|
|
83
|
+
<Plus className="w-4 h-4" />
|
|
84
|
+
<span>Ajouter un membre</span>
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Filters */}
|
|
89
|
+
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
|
90
|
+
<div className="flex-1 relative">
|
|
91
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
placeholder="Rechercher un membre..."
|
|
95
|
+
value={searchTerm}
|
|
96
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
97
|
+
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
<button className="flex items-center space-x-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
101
|
+
<Filter className="w-4 h-4" />
|
|
102
|
+
<span>Filtres</span>
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Team Stats */}
|
|
107
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
108
|
+
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
109
|
+
<h3 className="text-lg font-semibold text-gray-900">Total membres</h3>
|
|
110
|
+
<p className="text-3xl font-bold text-[#8290A9] mt-2">{teamMembers.length}</p>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
113
|
+
<h3 className="text-lg font-semibold text-gray-900">En ligne</h3>
|
|
114
|
+
<p className="text-3xl font-bold text-green-600 mt-2">
|
|
115
|
+
{teamMembers.filter(m => m.status === 'online').length}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
119
|
+
<h3 className="text-lg font-semibold text-gray-900">Projets actifs</h3>
|
|
120
|
+
<p className="text-3xl font-bold text-blue-600 mt-2">
|
|
121
|
+
{teamMembers.reduce((sum, m) => sum + m.projects, 0)}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Team Members Grid */}
|
|
127
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
128
|
+
{filteredMembers.map((member) => (
|
|
129
|
+
<div key={member.id} className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
130
|
+
<div className="flex items-center justify-between mb-4">
|
|
131
|
+
<div className="flex items-center space-x-3">
|
|
132
|
+
<div className="relative">
|
|
133
|
+
<div className="w-12 h-12 bg-[#8290A9] rounded-full flex items-center justify-center">
|
|
134
|
+
<span className="text-white font-semibold">
|
|
135
|
+
{getInitials(member.name)}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
<div className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-white ${getStatusColor(member.status)}`}></div>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<h3 className="font-semibold text-gray-900">{member.name}</h3>
|
|
142
|
+
<p className="text-sm text-gray-600">{member.role}</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
|
146
|
+
<MoreVertical className="w-4 h-4 text-gray-400" />
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="space-y-3">
|
|
151
|
+
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
|
152
|
+
<Mail className="w-4 h-4" />
|
|
153
|
+
<span>{member.email}</span>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
|
156
|
+
<Phone className="w-4 h-4" />
|
|
157
|
+
<span>{member.phone}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
|
160
|
+
<MapPin className="w-4 h-4" />
|
|
161
|
+
<span>{member.location}</span>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div className="mt-4 pt-4 border-t border-gray-100">
|
|
166
|
+
<div className="flex items-center justify-between">
|
|
167
|
+
<span className="text-sm text-gray-600">Projets actifs</span>
|
|
168
|
+
<span className="bg-[#8290A9] text-white px-2 py-1 rounded-full text-xs">
|
|
169
|
+
{member.projects}
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export default Team;
|