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.
Files changed (57) hide show
  1. package/eslint.config.js +28 -0
  2. package/index.html +78 -0
  3. package/package.json +42 -0
  4. package/postcss.config.js +6 -0
  5. package/src/App.tsx +156 -0
  6. package/src/assets/imgs/login_illustration.png +0 -0
  7. package/src/components/common/Buttons.tsx +39 -0
  8. package/src/components/common/Cards.tsx +18 -0
  9. package/src/components/common/FDrawer.tsx +2448 -0
  10. package/src/components/common/FDrawer.types.ts +191 -0
  11. package/src/components/common/Inputs.tsx +409 -0
  12. package/src/components/common/Modals.tsx +41 -0
  13. package/src/components/common/Navigations.tsx +0 -0
  14. package/src/components/common/Toast.tsx +0 -0
  15. package/src/components/demo/ToastDemo.tsx +73 -0
  16. package/src/components/layout/Header.tsx +202 -0
  17. package/src/components/layout/ModernDoubleSidebarLayout.tsx +727 -0
  18. package/src/components/layout/PrivateLayout.tsx +52 -0
  19. package/src/components/layout/Sidebar.tsx +182 -0
  20. package/src/components/ui/Toast.tsx +93 -0
  21. package/src/contexts/SessionContext.tsx +77 -0
  22. package/src/contexts/ThemeContext.tsx +58 -0
  23. package/src/contexts/ToastContext.tsx +94 -0
  24. package/src/index.css +3 -0
  25. package/src/main.tsx +10 -0
  26. package/src/models/Organization.ts +47 -0
  27. package/src/models/Plan.ts +42 -0
  28. package/src/models/User.ts +23 -0
  29. package/src/pages/Analytics.tsx +101 -0
  30. package/src/pages/CreateOrganization.tsx +215 -0
  31. package/src/pages/Dashboard.tsx +15 -0
  32. package/src/pages/Home.tsx +12 -0
  33. package/src/pages/Profile.tsx +313 -0
  34. package/src/pages/Settings.tsx +382 -0
  35. package/src/pages/Team.tsx +180 -0
  36. package/src/pages/auth/Login.tsx +140 -0
  37. package/src/pages/auth/Register.tsx +302 -0
  38. package/src/pages/organizations/DetailEntity.tsx +1002 -0
  39. package/src/pages/organizations/DetailOrganizations.tsx +1629 -0
  40. package/src/pages/organizations/ListOrganizations.tsx +270 -0
  41. package/src/pages/pricings/CartPlan.tsx +486 -0
  42. package/src/pages/pricings/ListPricing.tsx +321 -0
  43. package/src/pages/users/CreateUser.tsx +450 -0
  44. package/src/pages/users/ListUsers.tsx +0 -0
  45. package/src/services/AuthServices.ts +94 -0
  46. package/src/services/OrganizationServices.ts +61 -0
  47. package/src/services/PlanSubscriptionServices.tsx +137 -0
  48. package/src/services/UserServices.ts +36 -0
  49. package/src/services/api.ts +64 -0
  50. package/src/styles/theme.ts +383 -0
  51. package/src/utils/utils.ts +48 -0
  52. package/src/vite-env.d.ts +1 -0
  53. package/tailwind.config.js +158 -0
  54. package/tsconfig.app.json +24 -0
  55. package/tsconfig.json +7 -0
  56. package/tsconfig.node.json +22 -0
  57. package/vite.config.ts +10 -0
@@ -0,0 +1,1002 @@
1
+ import { useState, useEffect } from "react";
2
+ import { EntityServices, OrganizationServices } from "../../services/OrganizationServices";
3
+ import { UserServices } from "../../services/UserServices";
4
+ import { useNavigate, useParams } from "react-router-dom";
5
+ import { useToast } from "../../contexts/ToastContext";
6
+ import {useSession } from "../../contexts/SessionContext";
7
+ import { ArrowLeft, Building2, CreditCard, FileText, MapPin, User as UserIcon, X } from "lucide-react";
8
+ import { InputField, SelectInput, TextInput } from "../../components/common/Inputs";
9
+ import { RewiseBasicCard } from "../../components/common/Cards";
10
+ import { Entity } from "../../models/Organization";
11
+ import { User } from "../../models/User";
12
+
13
+
14
+
15
+ const DetailEntity: React.FC = () => {
16
+ const { id } = useParams<{ id: string }>();
17
+ const [entity, setEntity] = useState<Entity | null>(null);
18
+ const [formData, setFormData] = useState<Partial<Entity>>({});
19
+ const [entities, setEntities] = useState<Entity[]>([]);
20
+ const [users, setUsers] = useState<User[]>([]);
21
+ const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
22
+ const [loading, setLoading] = useState(true);
23
+ const [entitiesLoading, setEntitiesLoading] = useState(true);
24
+ const [usersLoading, setUsersLoading] = useState(true);
25
+ const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
26
+ const [showEntityModal, setShowEntityModal] = useState(false);
27
+ const [showCreateUserModal, setShowCreateUserModal] = useState(false);
28
+
29
+ const [errors, setErrors] = useState<Partial<Record<keyof Entity, string>>>({});
30
+
31
+
32
+ const [activeTab, setActiveTab] = useState<'general' | 'location' | 'banking' | 'fne' | 'modules' | 'billing' | 'users'>('general');
33
+
34
+ const navigate = useNavigate();
35
+ const { success, error: showError } = useToast();
36
+ const { token } = useSession();
37
+
38
+ useEffect(() => {
39
+ if (id) {
40
+ loadEntityDetails();
41
+ loadUsers();
42
+ }
43
+ }, [id]);
44
+
45
+
46
+ const tabs = [
47
+ { id: 'general', label: 'Général', icon: Building2 },
48
+ { id: 'location', label: 'Localisation', icon: MapPin },
49
+ { id: 'banking', label: 'Bancaire', icon: CreditCard },
50
+ { id: 'fne', label: 'FNE', icon: FileText },
51
+ { id: 'modules', label: 'Modules', icon: FileText },
52
+ { id: 'billing', label: 'Facturation', icon: CreditCard },
53
+ { id: 'users', label: 'Utilisateurs', icon: UserIcon }
54
+ ];
55
+
56
+
57
+
58
+ const validateForm = (): boolean => {
59
+ const newErrors: Partial<Record<keyof Entity, string>> = {};
60
+
61
+ if (!formData?.legal_name?.trim()) {
62
+ newErrors.legal_name = 'La raison sociale est obligatoire';
63
+ }
64
+
65
+ if (formData?.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
66
+ newErrors.email = 'Format d\'email invalide';
67
+ }
68
+
69
+ if (formData?.iban && formData.iban.length > 0 && formData.iban.length < 15) {
70
+ newErrors.iban = 'Format IBAN invalide';
71
+ }
72
+
73
+ setErrors(newErrors);
74
+ return Object.keys(newErrors).length === 0;
75
+ };
76
+
77
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
78
+ const { name, value } = e.target;
79
+ setFormData(prev => ({ ...prev, [name]: value }));
80
+ if (errors[name as keyof Entity]) {
81
+ setErrors(prev => ({ ...prev, [name]: undefined }));
82
+ }
83
+ };
84
+
85
+ const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
86
+ const { name, value } = e.target;
87
+ setFormData(prev => ({ ...prev, [name]: value }));
88
+ if (errors[name as keyof Entity]) {
89
+ setErrors(prev => ({ ...prev, [name]: undefined }));
90
+ }
91
+ };
92
+
93
+
94
+ const loadEntityDetails = async () => {
95
+ if (!token || !id) return;
96
+
97
+ try {
98
+ setLoading(true);
99
+ const result = await EntityServices.getEntity(parseInt(id), token) as { success: boolean, message: string, data: Entity };
100
+ setEntity(result.data);
101
+ setFormData(result.data);
102
+ console.log(result.data);
103
+
104
+ } catch (error: any) {
105
+ showError('Erreur lors du chargement des détails de l\'entité');
106
+ console.error(error);
107
+ } finally {
108
+ setLoading(false);
109
+ }
110
+ };
111
+ const loadUsers = async () => {
112
+ if (!token || !id) return;
113
+
114
+ try {
115
+ setUsersLoading(true);
116
+ const result = await EntityServices.getEntityUsers(parseInt(id), token) as { success: boolean, message: string, data: User[] };
117
+ setUsers(result.data);
118
+ } catch (error: any) {
119
+ showError('Erreur lors du chargement des utilisateurs de l\'entité');
120
+ console.error(error);
121
+ } finally {
122
+ setUsersLoading(false);
123
+ }
124
+ };
125
+
126
+
127
+ const loadOrganizationEntities = async () => {
128
+ if (!token || !id) return;
129
+
130
+ try {
131
+ setEntitiesLoading(true);
132
+ const result = await EntityServices.getOrganizationEntities(parseInt(id), token) as { success: boolean, message: string, data: Entity[] };
133
+ setEntities(result.data);
134
+ } catch (error: any) {
135
+ showError('Erreur lors du chargement des entités');
136
+ console.error(error);
137
+ } finally {
138
+ setEntitiesLoading(false);
139
+ }
140
+ };
141
+
142
+ const handleDeleteOrganization = async () => {
143
+ if (!token || !entity) return;
144
+
145
+ if (!window.confirm(`Êtes-vous sûr de vouloir supprimer l'entité "${entity.legal_name}" ?`)) {
146
+ return;
147
+ }
148
+
149
+ try {
150
+ await EntityServices.deleteEntity(entity.id, token);
151
+ success('Entité supprimée avec succès');
152
+ navigate('/organizations');
153
+ } catch (error: any) {
154
+ showError('Erreur lors de la suppression de l\'organisation');
155
+ }
156
+ };
157
+
158
+ const handleDeleteEntity = async (entityId: number, entityName: string) => {
159
+ if (!token) return;
160
+
161
+ if (!window.confirm(`Êtes-vous sûr de vouloir supprimer l'entité "${entityName}" ?`)) {
162
+ return;
163
+ }
164
+
165
+ try {
166
+ await EntityServices.deleteEntity(entityId, token);
167
+ success('Entité supprimée avec succès');
168
+ loadOrganizationEntities();
169
+ } catch (error: any) {
170
+ showError('Erreur lors de la suppression de l\'entit�');
171
+ }
172
+ };
173
+
174
+
175
+ const formatCurrency = (currency?: string) => {
176
+ const currencyMap: Record<string, string> = {
177
+ 'EUR': '€',
178
+ 'USD': '$',
179
+ 'GBP': '£',
180
+ 'CAD': 'CAD',
181
+ 'CHF': 'CHF',
182
+ 'XOF': 'XOF',
183
+ 'XAF': 'XAF'
184
+ };
185
+ return currencyMap[currency || ''] || currency || 'Non spécifié';
186
+ };
187
+
188
+ const handleUserCreated = () => {
189
+ setShowCreateUserModal(false);
190
+ loadUsers(); // Recharger la liste des utilisateurs
191
+ };
192
+
193
+
194
+ const handleSaveEntity = async (entityData: Partial<Entity>) => {
195
+
196
+
197
+ try {
198
+
199
+ await EntityServices.updateEntity(entity?.id!, entityData as any, token!);
200
+ success('Entité modifiée avec succès !');
201
+
202
+
203
+ } catch (error: any) {
204
+ console.error(error);
205
+ showError('Erreur lors de l\'enregistrement de l\'entité');
206
+ }
207
+ };
208
+
209
+ const handleSubmit = async (e: React.FormEvent) => {
210
+ e.preventDefault();
211
+ if (!validateForm()) return;
212
+
213
+ setLoading(true);
214
+ try {
215
+ await handleSaveEntity(formData);
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ };
220
+
221
+
222
+ const renderTabContent = () => {
223
+ switch (activeTab) {
224
+ case 'general':
225
+ return (
226
+ <div className="space-y-4">
227
+ <TextInput
228
+ label="Raison sociale"
229
+ name="legal_name"
230
+ value={formData.legal_name || ''}
231
+ placeholder="Nom légal de l'entité"
232
+ required
233
+ error={errors.legal_name}
234
+ onChange={handleInputChange}
235
+ />
236
+
237
+ <TextInput
238
+ label="Nom commercial"
239
+ name="trading_name"
240
+ value={formData.trading_name || ''}
241
+ placeholder="Nom commercial (optionnel)"
242
+ onChange={handleInputChange}
243
+ />
244
+
245
+
246
+
247
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
248
+ <TextInput
249
+ label="Numéro d'identification fiscale (NCC)"
250
+ name="tax_account"
251
+ value={formData.tax_account || ''}
252
+ placeholder="Numéro d'identification fiscale"
253
+ onChange={handleInputChange}
254
+ />
255
+
256
+ <TextInput
257
+ label="RCCM"
258
+ name="rccm"
259
+ value={formData.rccm || ''}
260
+ placeholder="Registre du Commerce et du Crédit Mobilier"
261
+ onChange={handleInputChange}
262
+ />
263
+
264
+ <InputField
265
+ label="Téléphone"
266
+ name="phone"
267
+ type="tel"
268
+ value={formData.phone || ''}
269
+ placeholder="+33 1 23 45 67 89"
270
+ onChange={handleInputChange}
271
+ />
272
+
273
+ <InputField
274
+ label="Email"
275
+ name="email"
276
+ type="email"
277
+ value={formData.email || ''}
278
+ placeholder="contact@entite.com"
279
+ error={errors.email}
280
+ onChange={handleInputChange}
281
+ />
282
+ </div>
283
+ </div>
284
+ );
285
+
286
+ case 'location':
287
+ return (
288
+ <div className="space-y-4">
289
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
290
+ <TextInput
291
+ label="Pays"
292
+ name="country"
293
+ value={formData.country || ''}
294
+ placeholder="Cote d'Ivoire"
295
+ onChange={handleInputChange}
296
+ />
297
+
298
+ <TextInput
299
+ label="Ville"
300
+ name="city"
301
+ value={formData.city || ''}
302
+ placeholder="Abidjan"
303
+ onChange={handleInputChange}
304
+ />
305
+ </div>
306
+
307
+ <div>
308
+ <label className="block text-sm font-medium text-gray-700 mb-2">
309
+ Adresse complète
310
+ </label>
311
+ <textarea
312
+ name="address"
313
+ value={formData.address || ''}
314
+ onChange={handleTextareaChange}
315
+ rows={3}
316
+ className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#6B7C92] focus:border-transparent placeholder-gray-400"
317
+ placeholder="Adresse complète de l'entité"
318
+ />
319
+ </div>
320
+ </div>
321
+ );
322
+
323
+ case 'banking':
324
+ return (
325
+ <div className="space-y-4">
326
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
327
+ <TextInput
328
+ label="RIB"
329
+ name="rib"
330
+ value={formData.rib || ''}
331
+ placeholder="Relevé d'Identité Bancaire"
332
+ onChange={handleInputChange}
333
+ />
334
+
335
+ <TextInput
336
+ label="IBAN"
337
+ name="iban"
338
+ value={formData.iban || ''}
339
+ placeholder="FR76 1234 5678 9012 3456 789"
340
+ error={errors.iban}
341
+ onChange={handleInputChange}
342
+ />
343
+ </div>
344
+
345
+ <TextInput
346
+ label="Nom de la banque"
347
+ name="bank_name"
348
+ value={formData.bank_name || ''}
349
+ placeholder="Nom de la banque"
350
+ onChange={handleInputChange}
351
+ />
352
+
353
+ <div>
354
+ <label className="block text-sm font-medium text-gray-700 mb-2">
355
+ Adresse de la banque
356
+ </label>
357
+ <textarea
358
+ name="bank_address"
359
+ value={formData.bank_address || ''}
360
+ onChange={handleTextareaChange}
361
+ rows={2}
362
+ className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#6B7C92] focus:border-transparent placeholder-gray-400"
363
+ placeholder="Adresse de la banque"
364
+ />
365
+ </div>
366
+
367
+
368
+ </div>
369
+ );
370
+
371
+ case 'fne':
372
+ return (
373
+ <div className="space-y-4">
374
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
375
+ <TextInput
376
+ label="Regime Fiscal"
377
+ name="regime_taxes"
378
+ value={formData.regime_taxes || ''}
379
+ placeholder=""
380
+ onChange={handleInputChange}
381
+ />
382
+ <TextInput
383
+ label="Centre de taxe"
384
+ name="centers_taxes"
385
+ value={formData.centers_taxes || ''}
386
+ placeholder=""
387
+ onChange={handleInputChange}
388
+ />
389
+ <TextInput
390
+ label="Centre de taxe"
391
+ name="centers_taxes"
392
+ value={formData.centers_taxes || ''}
393
+ placeholder=""
394
+ onChange={handleInputChange}
395
+ />
396
+
397
+
398
+ <TextInput
399
+ label={`Etablissement`}
400
+ name="establishment"
401
+ value={formData.establishment ?? ''}
402
+ onChange={handleInputChange}
403
+ />
404
+
405
+ <TextInput
406
+ label={`Point de vente`}
407
+ name="point_of_sale"
408
+ value={formData.point_of_sale ?? ''}
409
+ onChange={handleInputChange}
410
+ />
411
+
412
+ <TextInput
413
+ label={`Clé d'authentification FNE`}
414
+ name="fne_auth_key"
415
+ value={formData.fne_auth_key ?? ''}
416
+ onChange={handleInputChange}
417
+ />
418
+
419
+ <SelectInput
420
+ label={`STATUT FNE`}
421
+ name="fne_url"
422
+ value={formData.fne_url ?? ''}
423
+ options={[
424
+ { value: 'test', label: 'test' },
425
+ { value: 'prod', label: 'production' },
426
+ ]}
427
+ onChange={handleInputChange}
428
+ />
429
+
430
+
431
+ </div>
432
+ </div>
433
+ );
434
+
435
+ case 'modules':
436
+ return (
437
+ <div className="space-y-4">
438
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
439
+
440
+ </div>
441
+ </div>
442
+ );
443
+ case 'billing':
444
+ return (
445
+ <div className="space-y-4">
446
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
447
+
448
+ </div>
449
+ </div>
450
+ );
451
+ case 'users':
452
+ return (
453
+ <div className="space-y-4">
454
+ <div className="flex justify-between items-center">
455
+ <h4 className="text-lg font-medium text-gray-900">Utilisateurs de l'entité</h4>
456
+ {users.length > 0 && (
457
+ <button
458
+ onClick={() => setShowCreateUserModal(true)}
459
+ className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors text-sm"
460
+ >
461
+ Ajouter un utilisateur
462
+ </button>
463
+ )}
464
+ </div>
465
+
466
+ <div className="">
467
+ {usersLoading ? (
468
+ <div className="text-center py-8">
469
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
470
+ <p className="text-gray-600">Chargement des utilisateurs...</p>
471
+ </div>
472
+ ) : users.length === 0 ? (
473
+ <div className="text-center py-12">
474
+ <UserIcon className="w-12 h-12 text-gray-300 mx-auto mb-4" />
475
+ <h3 className="text-lg font-medium text-gray-900 mb-2">Aucun utilisateur</h3>
476
+ <p className="text-gray-600 mb-6">
477
+ Cette entité n'a pas encore d'utilisateurs associés.
478
+ </p>
479
+ <button
480
+ onClick={() => setShowCreateUserModal(true)}
481
+ className="bg-[#8290A9] text-white px-6 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
482
+ >
483
+ Ajouter un utilisateur
484
+ </button>
485
+ </div>
486
+ ) : (
487
+ <div className="overflow-x-auto">
488
+ <table className="min-w-full divide-y divide-gray-200">
489
+ <thead className="bg-gray-50">
490
+ <tr>
491
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">nom</th>
492
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Prénoms </th>
493
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">email</th>
494
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Numero de téléphone</th>
495
+
496
+
497
+ </tr>
498
+ </thead>
499
+ <tbody className="bg-white divide-y divide-gray-100">
500
+ {users.map((user) => (
501
+ <tr key={user.id} className="hover:bg-gray-50">
502
+ <td className="px-4 py-2 font-medium text-gray-900">{user.last_name}</td>
503
+ <td className="px-4 py-2 text-gray-600">{user.first_name || '-'}</td>
504
+ <td className="px-4 py-2 text-gray-600">{user.email}</td>
505
+ <td className="px-4 py-2 text-gray-600">{user.phonenumber || '-'}</td>
506
+
507
+
508
+
509
+ </tr>
510
+ ))}
511
+ </tbody>
512
+ </table>
513
+ </div>
514
+ )}
515
+ </div>
516
+
517
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
518
+ <TextInput
519
+ label="Nom"
520
+ name="name"
521
+ value={formData.name || ''}
522
+ placeholder=""
523
+ onChange={handleInputChange}
524
+ />
525
+ <TextInput
526
+ label="Email"
527
+ name="email"
528
+ value={formData.email || ''}
529
+ placeholder=""
530
+ onChange={handleInputChange}
531
+ />
532
+ <TextInput
533
+ label="Téléphone"
534
+ name="phone"
535
+ value={formData.phone || ''}
536
+ placeholder=""
537
+ onChange={handleInputChange}
538
+ />
539
+ <SelectInput
540
+ label="Rôle"
541
+ name="role"
542
+ value={formData.role || ''}
543
+ options={[
544
+ { value: 'admin', label: 'Administrateur' },
545
+ { value: 'user', label: 'Utilisateur' },
546
+ ]}
547
+ onChange={handleInputChange}
548
+ />
549
+ </div>
550
+ </div>
551
+ );
552
+
553
+ default:
554
+ return null;
555
+ }
556
+ };
557
+
558
+
559
+ if (loading) {
560
+ return (
561
+ <div className="space-y-6">
562
+ <div className="flex items-center justify-center min-h-[400px]">
563
+ <div className="text-center">
564
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
565
+ <p className="text-gray-600">Chargement des détails de l'organisation...</p>
566
+ </div>
567
+ </div>
568
+ </div>
569
+ );
570
+ }
571
+
572
+ if (!entity) {
573
+ return (
574
+ <div className="space-y-6">
575
+ <div className="flex items-center justify-center min-h-[400px]">
576
+ <div className="text-center">
577
+ <Building2 className="w-16 h-16 text-gray-300 mx-auto mb-4" />
578
+ <h3 className="text-lg font-medium text-gray-900 mb-2">Organisation non trouvée</h3>
579
+ <p className="text-gray-600 mb-6">L'organisation demandée n'existe pas ou vous n'y avez pas accès.</p>
580
+ <button
581
+ onClick={() => navigate('/organizations')}
582
+ className="bg-[#8290A9] text-white px-6 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
583
+ >
584
+ Retour aux organisations
585
+ </button>
586
+ </div>
587
+ </div>
588
+ </div>
589
+ );
590
+ }
591
+
592
+ return (
593
+ <div className="space-y-6">
594
+ {/* Header */}
595
+ <div className="flex items-center justify-between">
596
+ <div className="flex items-center space-x-4">
597
+ <button
598
+ onClick={() => navigate('/organizations')}
599
+ className="flex items-center text-gray-600 hover:text-gray-900 transition-colors"
600
+ >
601
+ <ArrowLeft className="w-4 h-4 mr-2" />
602
+ Retour aux organisations
603
+ </button>
604
+ </div>
605
+
606
+ <div className="flex items-center space-x-3">
607
+
608
+
609
+ </div>
610
+ </div>
611
+ <RewiseBasicCard title={<h6>Détails de l'entité {entity?.legal_name} </h6>}>
612
+ <div className="border-b border-gray-200">
613
+ <nav className="flex space-x-8 px-6">
614
+ {tabs.map((tab) => {
615
+ const Icon = tab.icon;
616
+ return (
617
+ <button
618
+ key={tab.id}
619
+ onClick={() => setActiveTab(tab.id as any)}
620
+ className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${activeTab === tab.id
621
+ ? 'border-[#8290A9] text-[#8290A9]'
622
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
623
+ }`}
624
+ >
625
+ <Icon className="w-4 h-4 inline mr-2" />
626
+ {tab.label}
627
+ </button>
628
+ );
629
+ })}
630
+ </nav>
631
+ </div>
632
+
633
+ {/* Content */}
634
+ <form onSubmit={handleSubmit} className="p-6">
635
+ {renderTabContent()}
636
+
637
+ {/* Actions */}
638
+ <div className="flex justify-between pt-6 border-t border-gray-200 mt-8">
639
+
640
+ <button
641
+ type="submit"
642
+ disabled={loading}
643
+ className="px-6 py-2 bg-[#8290A9] text-white rounded-lg hover:bg-[#6B7C92] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
644
+ >
645
+ {loading ? 'chargement...' : 'Enregistrer l\'entité'}
646
+ </button>
647
+ </div>
648
+ </form>
649
+
650
+
651
+ </RewiseBasicCard>
652
+
653
+
654
+
655
+
656
+ {/* Click outside to close dropdown */}
657
+ {dropdownOpen && (
658
+ <div
659
+ className="fixed inset-0 z-5"
660
+ onClick={() => setDropdownOpen(null)}
661
+ />
662
+ )}
663
+
664
+ {/* Create User Modal */}
665
+ {showCreateUserModal && (
666
+ <CreateUserModal
667
+ isOpen={showCreateUserModal}
668
+ onClose={() => setShowCreateUserModal(false)}
669
+ onUserCreated={handleUserCreated}
670
+ entityId={entity?.id}
671
+ />
672
+ )}
673
+
674
+ </div>
675
+ );
676
+ };
677
+
678
+ // CreateUserModal Component
679
+ interface CreateUserModalProps {
680
+ isOpen: boolean;
681
+ onClose: () => void;
682
+ onUserCreated: () => void;
683
+ entityId?: number;
684
+ }
685
+
686
+ const CreateUserModal: React.FC<CreateUserModalProps> = ({
687
+ isOpen,
688
+ onClose,
689
+ onUserCreated,
690
+ entityId
691
+ }) => {
692
+ const [formData, setFormData] = useState<Partial<User>>({
693
+ username: '',
694
+ first_name: '',
695
+ last_name: '',
696
+ email: '',
697
+ phonenumber: '',
698
+ is_active: true,
699
+ is_staff: false,
700
+ is_superuser: false,
701
+ groups: [],
702
+ user_permissions: [],
703
+ centers_access: []
704
+ });
705
+
706
+ const [errors, setErrors] = useState<Partial<Record<keyof User, string>>>({});
707
+ const [loading, setLoading] = useState(false);
708
+ const [activeTab, setActiveTab] = useState<'general' | 'permissions'>('general');
709
+
710
+ const { success, error: showError } = useToast();
711
+ const { token } = useSession();
712
+
713
+ const tabs = [
714
+ { id: 'general', label: 'Informations générales', icon: UserIcon },
715
+ { id: 'permissions', label: 'Permissions', icon: Building2 }
716
+ ];
717
+
718
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
719
+ const { name, value, type, checked } = e.target;
720
+ setFormData(prev => ({
721
+ ...prev,
722
+ [name]: type === 'checkbox' ? checked : value
723
+ }));
724
+
725
+ if (errors[name as keyof User]) {
726
+ setErrors(prev => ({ ...prev, [name]: undefined }));
727
+ }
728
+ };
729
+
730
+ const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
731
+ const { name, value } = e.target;
732
+ setFormData(prev => ({ ...prev, [name]: value === 'true' }));
733
+ };
734
+
735
+ const validateForm = (): boolean => {
736
+ const newErrors: Partial<Record<keyof User, string>> = {};
737
+
738
+ if (!formData.username?.trim()) {
739
+ newErrors.username = 'Le nom d\'utilisateur est obligatoire';
740
+ }
741
+
742
+ if (!formData.first_name?.trim()) {
743
+ newErrors.first_name = 'Le prénom est obligatoire';
744
+ }
745
+
746
+ if (!formData.last_name?.trim()) {
747
+ newErrors.last_name = 'Le nom de famille est obligatoire';
748
+ }
749
+
750
+ if (!formData.email?.trim()) {
751
+ newErrors.email = 'L\'email est obligatoire';
752
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
753
+ newErrors.email = 'Format d\'email invalide';
754
+ }
755
+
756
+ if (formData.phonenumber && !/^[\+]?[0-9\s\-\(\)]+$/.test(formData.phonenumber)) {
757
+ newErrors.phonenumber = 'Format de téléphone invalide';
758
+ }
759
+
760
+ setErrors(newErrors);
761
+ return Object.keys(newErrors).length === 0;
762
+ };
763
+
764
+ const handleSubmit = async (e: React.FormEvent) => {
765
+ e.preventDefault();
766
+ if (!validateForm()) return;
767
+
768
+ if (!token) {
769
+ showError('Vous devez être connecté pour créer un utilisateur');
770
+ return;
771
+ }
772
+
773
+ setLoading(true);
774
+ try {
775
+ // Créer l'utilisateur
776
+ const result = await UserServices.addUser(formData, token) as { success: boolean, data: User };
777
+
778
+ // Si un entityId est fourni, associer l'utilisateur à l'entité
779
+ if (entityId && result.data?.id) {
780
+ await EntityServices.addUserToEntity(entityId, result.data.id, token);
781
+ }
782
+
783
+ success('Utilisateur créé avec succès !');
784
+ onUserCreated();
785
+ } catch (error: any) {
786
+ console.error(error);
787
+ showError('Erreur lors de la création de l\'utilisateur');
788
+ } finally {
789
+ setLoading(false);
790
+ }
791
+ };
792
+
793
+ const renderTabContent = () => {
794
+ switch (activeTab) {
795
+ case 'general':
796
+ return (
797
+ <div className="space-y-4">
798
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
799
+ <TextInput
800
+ label="Nom d'utilisateur"
801
+ name="username"
802
+ value={formData.username || ''}
803
+ placeholder="nom.utilisateur"
804
+ required
805
+ error={errors.username}
806
+ onChange={handleInputChange}
807
+ />
808
+
809
+ <InputField
810
+ label="Email"
811
+ name="email"
812
+ type="email"
813
+ value={formData.email || ''}
814
+ placeholder="utilisateur@example.com"
815
+ required
816
+ error={errors.email}
817
+ onChange={handleInputChange}
818
+ />
819
+ </div>
820
+
821
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
822
+ <TextInput
823
+ label="Prénom"
824
+ name="first_name"
825
+ value={formData.first_name || ''}
826
+ placeholder="Prénom"
827
+ required
828
+ error={errors.first_name}
829
+ onChange={handleInputChange}
830
+ />
831
+
832
+ <TextInput
833
+ label="Nom de famille"
834
+ name="last_name"
835
+ value={formData.last_name || ''}
836
+ placeholder="Nom de famille"
837
+ required
838
+ error={errors.last_name}
839
+ onChange={handleInputChange}
840
+ />
841
+ </div>
842
+
843
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
844
+ <InputField
845
+ label="Téléphone"
846
+ name="phonenumber"
847
+ type="tel"
848
+ value={formData.phonenumber || ''}
849
+ placeholder="+225 XX XX XXX XXX"
850
+ error={errors.phonenumber}
851
+ onChange={handleInputChange}
852
+ />
853
+
854
+ <SelectInput
855
+ label="Statut"
856
+ name="is_active"
857
+ value={formData.is_active?.toString() || 'true'}
858
+ options={[
859
+ { value: 'true', label: 'Actif' },
860
+ { value: 'false', label: 'Inactif' }
861
+ ]}
862
+ onChange={handleSelectChange}
863
+ />
864
+ </div>
865
+ </div>
866
+ );
867
+
868
+ case 'permissions':
869
+ return (
870
+ <div className="space-y-4">
871
+ <div className="grid grid-cols-1 gap-4">
872
+ <label className="flex items-center space-x-3">
873
+ <input
874
+ type="checkbox"
875
+ name="is_staff"
876
+ checked={formData.is_staff || false}
877
+ onChange={handleInputChange}
878
+ className="rounded border-gray-300 text-[#8290A9] focus:ring-[#8290A9]"
879
+ />
880
+ <div>
881
+ <span className="text-sm font-medium text-gray-900">Membre du personnel</span>
882
+ <p className="text-xs text-gray-500">
883
+ Permet d'accéder à l'interface d'administration
884
+ </p>
885
+ </div>
886
+ </label>
887
+
888
+ <label className="flex items-center space-x-3">
889
+ <input
890
+ type="checkbox"
891
+ name="is_superuser"
892
+ checked={formData.is_superuser || false}
893
+ onChange={handleInputChange}
894
+ className="rounded border-gray-300 text-[#8290A9] focus:ring-[#8290A9]"
895
+ />
896
+ <div>
897
+ <span className="text-sm font-medium text-gray-900">Super utilisateur</span>
898
+ <p className="text-xs text-gray-500">
899
+ Accès complet à toutes les fonctionnalités
900
+ </p>
901
+ </div>
902
+ </label>
903
+ </div>
904
+ </div>
905
+ );
906
+
907
+ default:
908
+ return null;
909
+ }
910
+ };
911
+
912
+ if (!isOpen) return null;
913
+
914
+ return (
915
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
916
+ <div className="bg-white rounded-lg shadow-xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto">
917
+ {/* Header */}
918
+ <div className="p-6 border-b border-gray-200">
919
+ <div className="flex items-center justify-between">
920
+ <div className="flex items-center space-x-3">
921
+ <div className="w-10 h-10 bg-[#8290A9] rounded-full flex items-center justify-center">
922
+ <UserIcon className="w-5 h-5 text-white" />
923
+ </div>
924
+ <div>
925
+ <h2 className="text-xl font-semibold text-gray-900">Créer un utilisateur</h2>
926
+ <p className="text-sm text-gray-600">Ajoutez un nouveau membre à cette entité</p>
927
+ </div>
928
+ </div>
929
+ <button
930
+ onClick={onClose}
931
+ disabled={loading}
932
+ className="text-gray-400 hover:text-gray-600 transition-colors disabled:opacity-50"
933
+ >
934
+ <X className="w-6 h-6" />
935
+ </button>
936
+ </div>
937
+ </div>
938
+
939
+ {/* Tabs */}
940
+ <div className="border-b border-gray-200">
941
+ <nav className="flex space-x-8 px-6">
942
+ {tabs.map((tab) => {
943
+ const Icon = tab.icon;
944
+ return (
945
+ <button
946
+ key={tab.id}
947
+ type="button"
948
+ onClick={() => setActiveTab(tab.id as any)}
949
+ className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
950
+ activeTab === tab.id
951
+ ? 'border-[#8290A9] text-[#8290A9]'
952
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
953
+ }`}
954
+ >
955
+ <Icon className="w-4 h-4 inline mr-2" />
956
+ {tab.label}
957
+ </button>
958
+ );
959
+ })}
960
+ </nav>
961
+ </div>
962
+
963
+ {/* Content */}
964
+ <form onSubmit={handleSubmit} className="p-6">
965
+ {renderTabContent()}
966
+
967
+ {/* Actions */}
968
+ <div className="flex justify-between pt-6 border-t border-gray-200 mt-8">
969
+ <button
970
+ type="button"
971
+ onClick={onClose}
972
+ disabled={loading}
973
+ className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
974
+ >
975
+ Annuler
976
+ </button>
977
+
978
+ <button
979
+ type="submit"
980
+ disabled={loading}
981
+ className="px-6 py-2 bg-[#8290A9] text-white rounded-lg hover:bg-[#6B7C92] transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
982
+ >
983
+ {loading ? (
984
+ <>
985
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
986
+ <span>Création...</span>
987
+ </>
988
+ ) : (
989
+ <>
990
+ <UserIcon className="w-4 h-4" />
991
+ <span>Créer l'utilisateur</span>
992
+ </>
993
+ )}
994
+ </button>
995
+ </div>
996
+ </form>
997
+ </div>
998
+ </div>
999
+ );
1000
+ };
1001
+
1002
+ export default DetailEntity;