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,1629 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import {
4
+ ArrowLeft,
5
+ Building2,
6
+ Edit,
7
+ Trash2,
8
+ Plus,
9
+ MapPin,
10
+ User as UserIcon,
11
+ Eye,
12
+ CreditCard,
13
+ FileText,
14
+ MoreVertical,
15
+ X,
16
+ } from 'lucide-react';
17
+ import { useToast } from '../../contexts/ToastContext';
18
+ import { useSession } from '../../contexts/SessionContext';
19
+ import { OrganizationServices, EntityServices } from '../../services/OrganizationServices';
20
+ import { RewiseBasicCard } from '../../components/common/Cards';
21
+ import { InputField, TextInput, SelectInput } from '../../components/common/Inputs';
22
+ import ListPricing from '../pricings/ListPricing';
23
+ import { Entity, Organization } from '../../models/Organization';
24
+ import { Module } from '../../models/Plan';
25
+ import { ModuleServices } from '../../services/PlanSubscriptionServices';
26
+ import PrimaryButton, { SecondaryButton } from '../../components/common/Buttons';
27
+ import Modal from '../../components/common/Modals';
28
+
29
+
30
+ const DetailOrganizations: React.FC = () => {
31
+ const { id } = useParams<{ id: string }>();
32
+ const [organization, setOrganization] = useState<Organization | null>(null);
33
+ const [formData, setFormData] = useState<Partial<Organization | null>>(null);
34
+ const [entities, setEntities] = useState<Entity[]>([]);
35
+ const [modules, setModules] = useState<Module[]>([]);
36
+ const [modulesLoading, setModulesLoading] = useState(true);
37
+ const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
38
+ const [loading, setLoading] = useState(true);
39
+ const [entitiesLoading, setEntitiesLoading] = useState(true);
40
+ const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
41
+ const [showEntityModal, setShowEntityModal] = useState(false);
42
+ const [showModulePurchaseModal, setShowModulePurchaseModal] = useState(false);
43
+ const [selectedModuleForPurchase, setSelectedModuleForPurchase] = useState<Module | null>(null);
44
+ const [processingModulePurchase, setProcessingModulePurchase] = useState(false);
45
+ const [showPlanChangeModal, setShowPlanChangeModal] = useState(false);
46
+ const [availablePlans, setAvailablePlans] = useState<any[]>([]);
47
+ const [plansLoading, setPlansLoading] = useState(false);
48
+ const [processingPlanChange, setProcessingPlanChange] = useState(false);
49
+
50
+ const [errors, setErrors] = useState<Partial<Record<keyof Organization, string>>>({});
51
+
52
+ const navigate = useNavigate();
53
+ const { success, error: showError } = useToast();
54
+ const { token } = useSession();
55
+
56
+
57
+
58
+ const [activeTab, setActiveTab] = useState<'entities' | 'modules' | 'billing'>('entities');
59
+ const tabs = [
60
+ { id: 'entities', label: 'Entités', icon: Building2 },
61
+ { id: 'modules', label: 'Plan & Modules', icon: FileText },
62
+ { id: 'billing', label: 'Paiements', icon: CreditCard }
63
+ ];
64
+
65
+
66
+ useEffect(() => {
67
+ if (id) {
68
+ loadOrganizationDetails();
69
+ loadOrganizationEntities();
70
+ loadModules();
71
+ }
72
+ }, [id]);
73
+
74
+
75
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
76
+ const { name, value } = e.target;
77
+ setFormData(prev => ({ ...prev, [name]: value }));
78
+ if (errors[name as keyof Organization]) {
79
+ setErrors(prev => ({ ...prev, [name]: undefined }));
80
+ }
81
+ };
82
+
83
+ const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
84
+ const { name, value } = e.target;
85
+ setFormData(prev => ({ ...prev, [name]: value }));
86
+ if (errors[name as keyof Organization]) {
87
+ setErrors(prev => ({ ...prev, [name]: undefined }));
88
+ }
89
+ };
90
+
91
+
92
+ const loadOrganizationDetails = async () => {
93
+ if (!token || !id) return;
94
+
95
+ try {
96
+ setLoading(true);
97
+ const result = await OrganizationServices.getOrganization(parseInt(id), token) as { success: boolean, message: string, data: Organization };
98
+ setOrganization(result.data);
99
+ setFormData(result.data);
100
+ console.log(result.data);
101
+
102
+ } catch (error: any) {
103
+ showError('Erreur lors du chargement des détails de l\'organisation');
104
+ console.error(error);
105
+ } finally {
106
+ setLoading(false);
107
+ }
108
+ };
109
+
110
+ const loadOrganizationEntities = async () => {
111
+ if (!token || !id) return;
112
+
113
+ try {
114
+ setEntitiesLoading(true);
115
+ const result = await EntityServices.getOrganizationEntities(parseInt(id), token) as { success: boolean, message: string, data: Entity[] };
116
+ setEntities(result.data);
117
+ } catch (error: any) {
118
+ showError('Erreur lors du chargement des entités');
119
+ console.error(error);
120
+ } finally {
121
+ setEntitiesLoading(false);
122
+ }
123
+ };
124
+ const loadModules = async () => {
125
+ if (!token || !id) return;
126
+
127
+ try {
128
+ setModulesLoading(true);
129
+ const result = await ModuleServices.getAllModules(token) as { success: boolean, message: string, data: Module[] };
130
+ setModules(result.data);
131
+ } catch (error: any) {
132
+ showError('Erreur lors du chargement des modules');
133
+ console.error(error);
134
+ } finally {
135
+ setModulesLoading(false);
136
+ }
137
+ };
138
+
139
+ const handleDeleteOrganization = async () => {
140
+ if (!token || !organization) return;
141
+
142
+ if (!window.confirm(`Êtes-vous sûr de vouloir supprimer l'organisation "${organization.legal_name}" ?`)) {
143
+ return;
144
+ }
145
+
146
+ try {
147
+ await OrganizationServices.deleteOrganization(organization.id, token);
148
+ success('Organisation supprimée avec succès');
149
+ navigate('/organizations');
150
+ } catch (error: any) {
151
+ showError('Erreur lors de la suppression de l\'organisation');
152
+ }
153
+ };
154
+
155
+ const handleDeleteEntity = async (entityId: number, entityName: string) => {
156
+ if (!token) return;
157
+
158
+ if (!window.confirm(`Êtes-vous sûr de vouloir supprimer l'entité "${entityName}" ?`)) {
159
+ return;
160
+ }
161
+
162
+ try {
163
+ await EntityServices.deleteEntity(entityId, token);
164
+ success('Entité supprimée avec succès');
165
+ loadOrganizationEntities();
166
+ } catch (error: any) {
167
+ showError('Erreur lors de la suppression de l\'entit�');
168
+ }
169
+ };
170
+
171
+
172
+ const formatCurrency = (currency?: string) => {
173
+ const currencyMap: Record<string, string> = {
174
+ 'EUR': '€',
175
+ 'USD': '$',
176
+ 'GBP': '£',
177
+ 'CAD': 'CAD',
178
+ 'CHF': 'CHF',
179
+ 'XOF': 'XOF',
180
+ 'XAF': 'XAF'
181
+ };
182
+ return currencyMap[currency || ''] || currency || 'Non spécifié';
183
+ };
184
+
185
+ const handlePurchaseModule = (module: Module) => {
186
+ if (!organization?.active_subscription) {
187
+ showError('Aucun abonnement actif trouvé');
188
+ return;
189
+ }
190
+
191
+ setSelectedModuleForPurchase(module);
192
+ setShowModulePurchaseModal(true);
193
+ };
194
+
195
+ const confirmModulePurchase = async () => {
196
+ if (!selectedModuleForPurchase || !organization || !token) return;
197
+
198
+ setProcessingModulePurchase(true);
199
+ try {
200
+ // Simuler l'achat du module via l'API
201
+ await OrganizationServices.subscribe(organization.id, {
202
+ plan: organization.active_subscription!.plan.id,
203
+ billing_cycle: 'monthly',
204
+ extra_modules: [
205
+ ...(
206
+ Array.isArray(organization.active_subscription!.extra_modules)
207
+ ? organization.active_subscription!.extra_modules.map(m =>
208
+ typeof m === 'object' && m !== null && 'id' in m ? m.id : m
209
+ )
210
+ : []
211
+ ),
212
+ selectedModuleForPurchase.id
213
+ ]
214
+ }, token);
215
+
216
+ success(`Module "${selectedModuleForPurchase.name}" ajouté avec succès !`);
217
+ setShowModulePurchaseModal(false);
218
+ setSelectedModuleForPurchase(null);
219
+
220
+ // Recharger les détails de l'organisation pour mettre à jour la liste des modules
221
+ loadOrganizationDetails();
222
+ } catch (error) {
223
+ showError('Erreur lors de l\'achat du module');
224
+ console.error(error);
225
+ } finally {
226
+ setProcessingModulePurchase(false);
227
+ }
228
+ };
229
+
230
+ const calculateModulePrice = (module: Module, withTax: boolean = true): number => {
231
+ const basePrice = module.price_monthly;
232
+ return withTax ? basePrice * 1.18 : basePrice; // TVA 18%
233
+ };
234
+
235
+ const loadAvailablePlans = async () => {
236
+ if (!token) return;
237
+
238
+ try {
239
+ setPlansLoading(true);
240
+ // Utiliser les plans depuis ListPricing
241
+ const pricingCategories = [
242
+ {
243
+ id: 'personnel',
244
+ name: 'Personnel',
245
+ plans: [
246
+ {
247
+ id: 1,
248
+ code: 'rewise_pro',
249
+ name: 'Pro',
250
+ description: 'Améliorez la productivité et l\'apprentissage avec un accès supplémentaire.',
251
+ price: 100000,
252
+ originalPrice: 83333,
253
+ currency: 'FCFA',
254
+ period: '/ mois',
255
+ features: [
256
+ { name: '1 entité', included: true },
257
+ { name: 'jusqu\'à 5 utilisateurs', included: true },
258
+ { name: 'CRM, Comptabilité et Facturation', included: true },
259
+ { name: '+1 module au choix', included: false },
260
+ ]
261
+ },
262
+ {
263
+ id: 2,
264
+ code: 'rewise_max',
265
+ name: 'Max',
266
+ description: 'Débloquez toutes les capacités avec un accès anticipé aux nouveaux produits.',
267
+ price: 200000,
268
+ originalPrice: 166667,
269
+ currency: 'FCFA',
270
+ period: '/ mois',
271
+ popular: true,
272
+ features: [
273
+ { name: 'jusqu\'à 3 entités', included: true },
274
+ { name: 'jusqu\'à 15 utilisateurs', included: true },
275
+ { name: 'CRM, Comptabilité et Facturation', included: true },
276
+ { name: '+1 module au choix', included: true },
277
+ ]
278
+ }
279
+ ]
280
+ }
281
+ ];
282
+
283
+ setAvailablePlans(pricingCategories[0].plans);
284
+ } catch (error: any) {
285
+ showError('Erreur lors du chargement des plans');
286
+ console.error(error);
287
+ } finally {
288
+ setPlansLoading(false);
289
+ }
290
+ };
291
+
292
+ const handleChangePlan = () => {
293
+ if (!organization?.active_subscription) {
294
+ showError('Aucun abonnement actif trouvé');
295
+ return;
296
+ }
297
+
298
+ loadAvailablePlans();
299
+ setShowPlanChangeModal(true);
300
+ };
301
+
302
+ const confirmPlanChange = async (newPlan: any) => {
303
+ if (!organization || !token) return;
304
+
305
+ setProcessingPlanChange(true);
306
+ try {
307
+ await OrganizationServices.subscribe(organization.id, {
308
+ plan: newPlan.id,
309
+ billing_cycle: 'monthly',
310
+ extra_modules: Array.isArray(organization.active_subscription!.extra_modules)
311
+ ? organization.active_subscription!.extra_modules.map(m =>
312
+ typeof m === 'object' && m !== null && 'id' in m ? m.id : m
313
+ )
314
+ : []
315
+ }, token);
316
+
317
+ success(`Plan changé vers "${newPlan.name}" avec succès !`);
318
+ setShowPlanChangeModal(false);
319
+
320
+ // Recharger les détails de l'organisation
321
+ loadOrganizationDetails();
322
+ } catch (error) {
323
+ showError('Erreur lors du changement de plan');
324
+ console.error(error);
325
+ } finally {
326
+ setProcessingPlanChange(false);
327
+ }
328
+ };
329
+
330
+
331
+ const renderTabContent = () => {
332
+ switch (activeTab) {
333
+ case 'entities':
334
+ return (
335
+ <RewiseBasicCard title={
336
+ <div className="w-full flex items-center justify-between">
337
+ <div>
338
+ <h6 className="">Entités</h6>
339
+
340
+ </div>
341
+ <PrimaryButton
342
+ type="button"
343
+ onClick={() => setShowEntityModal(true)}
344
+ >
345
+ <Plus className="w-4 h-4" /><span>Nouvelle Entité</span>
346
+ </PrimaryButton>
347
+ </div>
348
+ } >
349
+
350
+
351
+ <div className="">
352
+ {entitiesLoading ? (
353
+ <div className="text-center py-8">
354
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
355
+ <p className="text-gray-600">Chargement des entités...</p>
356
+ </div>
357
+ ) : entities.length === 0 ? (
358
+ <div className="text-center py-12">
359
+ <Building2 className="w-12 h-12 text-gray-300 mx-auto mb-4" />
360
+ <h3 className="text-lg font-medium text-gray-900 mb-2">Aucune entité</h3>
361
+ <p className="text-gray-600 mb-6">
362
+ Cette organisation n'a pas encore d'entités associées.
363
+ </p>
364
+ <button
365
+ onClick={() => navigate(`/organizations/${organization!.id}/entities/create`)}
366
+ className="bg-[#8290A9] text-white px-6 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
367
+ >
368
+ Créer une entité
369
+ </button>
370
+ </div>
371
+ ) : (
372
+ <div className="overflow-x-auto">
373
+ <table className="min-w-full divide-y divide-gray-200">
374
+ <thead className="bg-gray-50">
375
+ <tr>
376
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Raison sociale</th>
377
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Nom commercial</th>
378
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
379
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Téléphone</th>
380
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Pays / Ville</th>
381
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Devise</th>
382
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
383
+ </tr>
384
+ </thead>
385
+ <tbody className="bg-white divide-y divide-gray-100">
386
+ {entities.map((entity) => (
387
+ <tr key={entity.id} className="hover:bg-gray-50">
388
+ <td className="px-4 py-2 font-medium text-gray-900">{entity.legal_name}</td>
389
+ <td className="px-4 py-2 text-gray-600">{entity.trading_name || '-'}</td>
390
+ <td className="px-4 py-2 text-gray-600">{entity.email || '-'}</td>
391
+ <td className="px-4 py-2 text-gray-600">{entity.phone || '-'}</td>
392
+ <td className="px-4 py-2 text-gray-600">
393
+ {entity.country || '-'}
394
+ {entity.city ? `, ${entity.city}` : ''}
395
+ </td>
396
+ <td className="px-4 py-2 text-gray-600">{formatCurrency(entity.currency)}</td>
397
+ <td className="px-4 py-2">
398
+ <div className="relative">
399
+ <button
400
+ onClick={() => setDropdownOpen(dropdownOpen === entity.id ? null : entity.id)}
401
+ className="p-1 rounded-full hover:bg-gray-100 transition-colors"
402
+ >
403
+ <MoreVertical className="w-4 h-4 text-gray-400" />
404
+ </button>
405
+
406
+ {dropdownOpen === entity.id && (
407
+ <div className="fixed right-3 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-10">
408
+ <button
409
+ onClick={() => { navigate(`/entities/${entity.id}`); setDropdownOpen(null); }}
410
+ className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center space-x-2"
411
+ >
412
+ <Eye className="w-4 h-4" />
413
+ <span>Voir les détails</span>
414
+ </button>
415
+ <button
416
+ onClick={() => { setSelectedEntity(entity); setShowEntityModal(true); }}
417
+ className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center space-x-2"
418
+ >
419
+ <Edit className="w-4 h-4" />
420
+ <span>Modifier</span>
421
+ </button>
422
+ <button
423
+ onClick={() => handleDeleteEntity(entity.id, entity.legal_name)}
424
+ className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center space-x-2"
425
+ >
426
+ <Trash2 className="w-4 h-4" />
427
+ <span>Supprimer</span>
428
+ </button>
429
+ </div>
430
+ )}
431
+ </div>
432
+
433
+ </td>
434
+ </tr>
435
+ ))}
436
+ </tbody>
437
+ </table>
438
+ </div>
439
+ )}
440
+ </div>
441
+
442
+
443
+ </RewiseBasicCard>
444
+ );
445
+
446
+
447
+
448
+ case 'modules':
449
+ return (
450
+ <RewiseBasicCard title={
451
+ <div className="w-full flex items-center justify-between">
452
+ <div>
453
+ <h6 className="">Plan & Modules</h6>
454
+ </div>
455
+
456
+ </div>
457
+
458
+
459
+ }>
460
+ {/* <p>Gestion des modules et fonctionnalités activées pour cette organisation.</p> */}
461
+ {organization?.active_subscription && (
462
+ <div className="mb-6">
463
+ <div className="bg-[#F5F7FA] rounded-lg p-4 flex items-center justify-between shadow-sm">
464
+ <div>actif
465
+ <div className="font-semibold text-lg text-[#8290A9]">{organization.active_subscription.plan.name}</div>
466
+ <div className="text-sm text-gray-500">{organization.active_subscription.plan.description}</div>
467
+ <div className="text-sm text-gray-700">
468
+ {organization.active_subscription.plan.price_monthly} / mois
469
+ </div>
470
+ </div>
471
+ <button
472
+ className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
473
+ onClick={handleChangePlan}
474
+ >
475
+ Changer de plan
476
+ </button>
477
+ </div>
478
+ </div>
479
+ )}
480
+
481
+
482
+ <div className=" gap-4">
483
+ <div className="">
484
+ {modulesLoading ? (
485
+ <div className="text-center py-8">
486
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
487
+ <p className="text-gray-600">Chargement des modules...</p>
488
+ </div>
489
+ ) : modules.length === 0 ? (
490
+ <div className="text-center py-12">
491
+ <Building2 className="w-12 h-12 text-gray-300 mx-auto mb-4" />
492
+ <h3 className="text-lg font-medium text-gray-900 mb-2">Aucun module</h3>
493
+ <p className="text-gray-600 mb-6">
494
+ Aucun module n'est disponible pour le moment.
495
+ </p>
496
+ <button
497
+ onClick={() => navigate(`/organizations/${organization!.id}/modules/create`)}
498
+ className="bg-[#8290A9] text-white px-6 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
499
+ >
500
+ Créer une entité
501
+ </button>
502
+ </div>
503
+ ) : (
504
+ <div className="overflow-x-auto">
505
+ <table className="min-w-full divide-y divide-gray-200">
506
+ <thead className="bg-gray-50">
507
+ <tr>
508
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">nom</th>
509
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">description</th>
510
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Montant</th>
511
+
512
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
513
+
514
+ </tr>
515
+ </thead>
516
+ <tbody className="bg-white divide-y divide-gray-100">
517
+ {modules.map((module) => (
518
+ <tr key={module.id} className="hover:bg-gray-50">
519
+ <td className="px-4 py-2 font-medium text-gray-900">{module.name}</td>
520
+ <td className="px-4 py-2 text-gray-600">{module.description || '-'}</td>
521
+ <td className="px-4 py-2 text-gray-600">{module.price_monthly}</td>
522
+
523
+ <td className="px-4 py-2 text-gray-600">
524
+ {organization?.active_subscription && !organization!.modules!.some(m => m.id === module.id) ? (
525
+ <PrimaryButton onClick={() => handlePurchaseModule(module)} >Obtenir</PrimaryButton>
526
+ ) : organization?.active_subscription && organization!.modules!.some(m => m.id === module.id) ? (
527
+ <span className="text-green-600 text-sm font-medium">✓ Activé</span>
528
+ ) : (
529
+ <span className="text-gray-400 text-sm">Non disponible</span>
530
+ )}
531
+ </td>
532
+ </tr>
533
+ ))}
534
+ </tbody>
535
+ </table>
536
+ </div>
537
+ )}
538
+ </div>
539
+
540
+ </div>
541
+ </RewiseBasicCard>
542
+ );
543
+ case 'billing':
544
+ return (
545
+ <RewiseBasicCard title={<h6>Paiements</h6>}>
546
+ <div className="">
547
+ {entitiesLoading ? (
548
+ <div className="text-center py-8">
549
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
550
+ <p className="text-gray-600">Chargement des entités...</p>
551
+ </div>
552
+ ) : entities.length === 0 ? (
553
+ <div className="text-center py-12">
554
+ <Building2 className="w-12 h-12 text-gray-300 mx-auto mb-4" />
555
+ <h3 className="text-lg font-medium text-gray-900 mb-2">Aucune entité</h3>
556
+ <p className="text-gray-600 mb-6">
557
+ Cette organisation n'a pas encore d'entités associées.
558
+ </p>
559
+ <button
560
+ onClick={() => navigate(`/organizations/${organization!.id}/entities/create`)}
561
+ className="bg-[#8290A9] text-white px-6 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
562
+ >
563
+ Créer une entité
564
+ </button>
565
+ </div>
566
+ ) : (
567
+ <div className="overflow-x-auto">
568
+ <table className="min-w-full divide-y divide-gray-200">
569
+ <thead className="bg-gray-50">
570
+ <tr>
571
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
572
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Methode de paiement</th>
573
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Montant</th>
574
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">statut</th>
575
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Factures</th>
576
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Recu de paiement</th>
577
+
578
+ </tr>
579
+ </thead>
580
+ <tbody className="bg-white divide-y divide-gray-100">
581
+ {[].map((payment) => (
582
+ <tr key={payment.id} className="hover:bg-gray-50">
583
+ <td className="px-4 py-2 font-medium text-gray-900">{payment.date}</td>
584
+ <td className="px-4 py-2 text-gray-600">{payment.method || '-'}</td>
585
+ <td className="px-4 py-2 text-gray-600">{formatCurrency(payment.amount)}</td>
586
+ <td className="px-4 py-2 text-gray-600">{payment.status || '-'}</td>
587
+ <td className="px-4 py-2 text-gray-600">
588
+ {payment.invoice || '-'}
589
+ </td>
590
+ <td className="px-4 py-2 text-gray-600">{formatCurrency(payment.receipt)}</td>
591
+
592
+ </tr>
593
+ ))}
594
+ </tbody>
595
+ </table>
596
+ </div>
597
+ )}
598
+ </div>
599
+ </RewiseBasicCard>
600
+ );
601
+
602
+ default:
603
+ return null;
604
+ }
605
+ };
606
+
607
+
608
+
609
+
610
+
611
+ if (loading) {
612
+ return (
613
+ <div className="space-y-6">
614
+ <div className="flex items-center justify-center min-h-[400px]">
615
+ <div className="text-center">
616
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
617
+ <p className="text-gray-600">Chargement des détails de l'organisation...</p>
618
+ </div>
619
+ </div>
620
+ </div>
621
+ );
622
+ }
623
+
624
+ if (organization && organization.active_subscription === null) {
625
+ return (
626
+ <div id='' className="relative space-y-6">
627
+ <X onClick={() => navigate('/organizations')} className="absolute right-4 top-4 w-8 h-8 text-gray-300 mx-auto mb-4" />
628
+ <ListPricing organizationId={organization.id} />
629
+ </div>
630
+ );
631
+ }
632
+
633
+ if (!organization) {
634
+ return (
635
+ <div className="space-y-6">
636
+ <div className="flex items-center justify-center min-h-[400px]">
637
+ <div className="text-center">
638
+ <Building2 className="w-16 h-16 text-gray-300 mx-auto mb-4" />
639
+ <h3 className="text-lg font-medium text-gray-900 mb-2">Organisation non trouvée</h3>
640
+ <p className="text-gray-600 mb-6">L'organisation demandée n'existe pas ou vous n'y avez pas accès.</p>
641
+ <button
642
+ onClick={() => navigate('/organizations')}
643
+ className="bg-[#8290A9] text-white px-6 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors"
644
+ >
645
+ Retour aux organisations
646
+ </button>
647
+ </div>
648
+ </div>
649
+ </div>
650
+ );
651
+ }
652
+
653
+ return (
654
+ <div className="space-y-6">
655
+ {/* Header */}
656
+ <div className="flex items-center justify-between">
657
+ <div className="flex items-center space-x-4">
658
+ <button
659
+ onClick={() => navigate('/organizations')}
660
+ className="flex items-center text-gray-600 hover:text-gray-900 transition-colors"
661
+ >
662
+ <ArrowLeft className="w-4 h-4 mr-2" />
663
+ Retour aux organisations
664
+ </button>
665
+ </div>
666
+
667
+ <div className="flex items-center space-x-3">
668
+ <button
669
+ onClick={() => navigate(`/organizations/${organization.id}/edit`)}
670
+ className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors flex items-center space-x-2"
671
+ >
672
+ <Edit className="w-4 h-4" />
673
+ <span>Modifier</span>
674
+ </button>
675
+ <button
676
+ onClick={handleDeleteOrganization}
677
+ className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2"
678
+ >
679
+ <Trash2 className="w-4 h-4" />
680
+ <span>Supprimer</span>
681
+ </button>
682
+ </div>
683
+ </div>
684
+
685
+ {/* Organization Details */}
686
+ <RewiseBasicCard title={<h6>Détails de l'organisation</h6>}>
687
+
688
+ <div className="flex items-center space-x-3 mb-6">
689
+
690
+ <div className='w-[60%]'>
691
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-3">
692
+
693
+ {/* Raison sociale */}
694
+ <TextInput
695
+ label="Raison sociale"
696
+ name="legal_name"
697
+ value={formData?.legal_name ?? ''}
698
+ placeholder="Nom légal de l'organisation"
699
+ required
700
+ error={errors.legal_name}
701
+ onChange={handleInputChange}
702
+ />
703
+
704
+ {/* Nom commercial */}
705
+ <TextInput
706
+ label="Nom commercial"
707
+ name="trading_name"
708
+ value={formData?.trading_name ?? ''}
709
+ placeholder="Nom commercial (optionnel)"
710
+ onChange={handleInputChange}
711
+ />
712
+
713
+ </div>
714
+
715
+ {/* Contact */}
716
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-3">
717
+ <InputField
718
+ label="Téléphone"
719
+ name="phone"
720
+ type="tel"
721
+ value={formData?.phone ?? ''}
722
+ placeholder="+225 XX XX XXX XXX"
723
+ onChange={handleInputChange}
724
+ />
725
+
726
+ <InputField
727
+ label="Email"
728
+ name="email"
729
+ type="email"
730
+ value={formData?.email ?? ''}
731
+ placeholder="contact@organisation.com"
732
+ error={errors.email}
733
+ onChange={handleInputChange}
734
+ />
735
+ </div>
736
+
737
+
738
+
739
+ </div>
740
+
741
+ <div className="w-[40%]">
742
+ {/* Adresse */}
743
+ <div>
744
+ <label className="block text-sm font-medium text-gray-700 mb-2">
745
+ Adresse
746
+ </label>
747
+ <textarea
748
+ name="address"
749
+ value={formData?.address ?? ''}
750
+ onChange={handleTextareaChange}
751
+ rows={4}
752
+ 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"
753
+ placeholder="Adresse complète de l'organisation"
754
+ />
755
+ </div>
756
+ </div>
757
+
758
+ </div>
759
+ </RewiseBasicCard>
760
+
761
+
762
+
763
+
764
+ <div className="border-b border-gray-200">
765
+ <nav className="flex space-x-8 px-6">
766
+ {tabs.map((tab) => {
767
+ const Icon = tab.icon;
768
+ return (
769
+ <button
770
+ key={tab.id}
771
+ onClick={() => setActiveTab(tab.id as any)}
772
+ className={`
773
+ flex items-center space-x-2 py-4 px-2 text-sm font-medium border-b-2 transition-colors
774
+ ${activeTab === tab.id
775
+ ? 'border-[#6A8A82] text-[#6A8A82]'
776
+ : 'border-transparent text-[#767676] hover:text-[#6A8A82] hover:border-[#6A8A82]/30'
777
+ }
778
+ `}
779
+ >
780
+ <Icon className="w-4 h-4 inline mr-2" />
781
+ {tab.label}
782
+ </button>
783
+ );
784
+ })}
785
+ </nav>
786
+ </div>
787
+
788
+ {renderTabContent()}
789
+
790
+
791
+
792
+
793
+
794
+
795
+ {/* Click outside to close dropdown */}
796
+ {dropdownOpen && (
797
+ <div
798
+ className="fixed inset-0 z-5"
799
+ onClick={() => setDropdownOpen(null)}
800
+ />
801
+ )}
802
+
803
+ {/* Entity Modal */}
804
+ {showEntityModal && (
805
+ <EntityModal
806
+ key={`entity-modal-${selectedEntity ? selectedEntity.id : 'new'}-${organization?.id}`}
807
+ object={selectedEntity}
808
+ isOpen={showEntityModal}
809
+ onClose={() => setShowEntityModal(false)}
810
+ organization={organization}
811
+ />
812
+ )}
813
+
814
+ {/* Module Purchase Modal */}
815
+ {showModulePurchaseModal && selectedModuleForPurchase && (
816
+ <ModulePurchaseModal
817
+ module={selectedModuleForPurchase}
818
+ organization={organization}
819
+ isOpen={showModulePurchaseModal}
820
+ onClose={() => {
821
+ setShowModulePurchaseModal(false);
822
+ setSelectedModuleForPurchase(null);
823
+ }}
824
+ onConfirm={confirmModulePurchase}
825
+ processing={processingModulePurchase}
826
+ />
827
+ )}
828
+
829
+ {/* Plan Change Modal */}
830
+ {showPlanChangeModal && (
831
+ <PlanChangeModal
832
+ plans={availablePlans}
833
+ currentPlan={organization?.active_subscription?.plan}
834
+ organization={organization}
835
+ isOpen={showPlanChangeModal}
836
+ onClose={() => setShowPlanChangeModal(false)}
837
+ onConfirm={confirmPlanChange}
838
+ processing={processingPlanChange}
839
+ plansLoading={plansLoading}
840
+ />
841
+ )}
842
+ </div>
843
+ );
844
+ };
845
+
846
+ export default DetailOrganizations;
847
+
848
+
849
+
850
+ // EntityModal Component
851
+ const EntityModal: React.FC<{ isOpen: boolean; onClose: () => void; object: Entity | null; organization: Organization | null }> = ({
852
+ isOpen,
853
+ onClose,
854
+ object,
855
+ organization
856
+ }) => {
857
+
858
+
859
+ const [formData, setFormData] = useState<Partial<Entity>>(object || {
860
+ organization: organization?.id,
861
+ legal_name: '',
862
+ trading_name: '',
863
+ phone: '',
864
+ email: '',
865
+ country: '',
866
+ city: '',
867
+ address: '',
868
+ rib: '',
869
+ iban: '',
870
+ bank_name: '',
871
+ bank_address: '',
872
+ tax_account: '',
873
+ rccm: '',
874
+ currency: 'EUR',
875
+ });
876
+
877
+ const [errors, setErrors] = useState<Partial<Record<keyof Entity, string>>>({});
878
+ const [activeTab, setActiveTab] = useState<'general' | 'location' | 'banking' | 'fne'>('general');
879
+ const [loading, setLoading] = useState(false);
880
+ const { token } = useSession();
881
+ const { success, error: showError } = useToast();
882
+
883
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
884
+ const { name, value } = e.target;
885
+ setFormData(prev => ({ ...prev, [name]: value }));
886
+ if (errors[name as keyof Entity]) {
887
+ setErrors(prev => ({ ...prev, [name]: undefined }));
888
+ }
889
+ };
890
+
891
+ const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
892
+ const { name, value } = e.target;
893
+ setFormData(prev => ({ ...prev, [name]: value }));
894
+ };
895
+
896
+ const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
897
+ const { name, value } = e.target;
898
+ setFormData(prev => ({ ...prev, [name]: value }));
899
+ };
900
+
901
+ const validateForm = (): boolean => {
902
+ const newErrors: Partial<Record<keyof Entity, string>> = {};
903
+
904
+ if (!formData.legal_name?.trim()) {
905
+ newErrors.legal_name = 'La raison sociale est obligatoire';
906
+ }
907
+
908
+ if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
909
+ newErrors.email = 'Format d\'email invalide';
910
+ }
911
+
912
+ if (formData.iban && formData.iban.length > 0 && formData.iban.length < 15) {
913
+ newErrors.iban = 'Format IBAN invalide';
914
+ }
915
+
916
+ setErrors(newErrors);
917
+ return Object.keys(newErrors).length === 0;
918
+ };
919
+
920
+ const handleSaveEntity = async (entityData: Partial<Entity>) => {
921
+
922
+
923
+ try {
924
+ if (object && object.id) {
925
+ // Modification
926
+ await EntityServices.updateEntity(object.id, entityData as any, token);
927
+ success('Entité modifiée avec succès !');
928
+ } else {
929
+ // Création
930
+ await EntityServices.createEntity(organization!.id, entityData as any, token);
931
+ success('Entité créée avec succès !');
932
+ }
933
+ onClose();
934
+ } catch (error: any) {
935
+ console.error(error);
936
+ showError('Erreur lors de l\'enregistrement de l\'entité');
937
+ }
938
+ };
939
+
940
+ const handleSubmit = async (e: React.FormEvent) => {
941
+ e.preventDefault();
942
+ if (!validateForm()) return;
943
+
944
+ setLoading(true);
945
+ try {
946
+ await handleSaveEntity(formData);
947
+ } finally {
948
+ setLoading(false);
949
+ }
950
+ };
951
+
952
+ const tabs = [
953
+ { id: 'general', label: 'Général', icon: Building2 },
954
+ { id: 'location', label: 'Localisation', icon: MapPin },
955
+ { id: 'banking', label: 'Bancaire', icon: CreditCard },
956
+ { id: 'fne', label: 'FNE', icon: FileText }
957
+ ];
958
+
959
+ const currencies = [
960
+ { value: 'EUR', label: 'Euro (€)' },
961
+ { value: 'USD', label: 'Dollar US ($)' },
962
+ { value: 'GBP', label: 'Livre Sterling (£)' },
963
+ { value: 'CAD', label: 'Dollar Canadien (CAD)' },
964
+ { value: 'CHF', label: 'Franc Suisse (CHF)' },
965
+ { value: 'XOF', label: 'Franc CFA (XOF)' },
966
+ { value: 'XAF', label: 'Franc CFA Central (XAF)' }
967
+ ];
968
+
969
+ const renderTabContent = () => {
970
+ switch (activeTab) {
971
+ case 'general':
972
+ return (
973
+ <div className="space-y-4">
974
+ <TextInput
975
+ label="Raison sociale"
976
+ name="legal_name"
977
+ value={formData.legal_name || ''}
978
+ placeholder="Nom légal de l'entité"
979
+ required
980
+ error={errors.legal_name}
981
+ onChange={handleInputChange}
982
+ />
983
+
984
+ <TextInput
985
+ label="Nom commercial"
986
+ name="trading_name"
987
+ value={formData.trading_name || ''}
988
+ placeholder="Nom commercial (optionnel)"
989
+ onChange={handleInputChange}
990
+ />
991
+
992
+
993
+
994
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
995
+ <TextInput
996
+ label="Numéro d'identification fiscale (NCC)"
997
+ name="tax_account"
998
+ value={formData.tax_account || ''}
999
+ placeholder="Numéro d'identification fiscale"
1000
+ onChange={handleInputChange}
1001
+ />
1002
+
1003
+ <TextInput
1004
+ label="RCCM"
1005
+ name="rccm"
1006
+ value={formData.rccm || ''}
1007
+ placeholder="Registre du Commerce et du Crédit Mobilier"
1008
+ onChange={handleInputChange}
1009
+ />
1010
+
1011
+ <InputField
1012
+ label="Téléphone"
1013
+ name="phone"
1014
+ type="tel"
1015
+ value={formData.phone || ''}
1016
+ placeholder="+33 1 23 45 67 89"
1017
+ onChange={handleInputChange}
1018
+ />
1019
+
1020
+ <InputField
1021
+ label="Email"
1022
+ name="email"
1023
+ type="email"
1024
+ value={formData.email || ''}
1025
+ placeholder="contact@entite.com"
1026
+ error={errors.email}
1027
+ onChange={handleInputChange}
1028
+ />
1029
+ </div>
1030
+ </div>
1031
+ );
1032
+
1033
+ case 'location':
1034
+ return (
1035
+ <div className="space-y-4">
1036
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1037
+ <TextInput
1038
+ label="Pays"
1039
+ name="country"
1040
+ value={formData.country || ''}
1041
+ placeholder="Cote d'Ivoire"
1042
+ onChange={handleInputChange}
1043
+ />
1044
+
1045
+ <TextInput
1046
+ label="Ville"
1047
+ name="city"
1048
+ value={formData.city || ''}
1049
+ placeholder="Abidjan"
1050
+ onChange={handleInputChange}
1051
+ />
1052
+ </div>
1053
+
1054
+ <div>
1055
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1056
+ Adresse complète
1057
+ </label>
1058
+ <textarea
1059
+ name="address"
1060
+ value={formData.address || ''}
1061
+ onChange={handleTextareaChange}
1062
+ rows={3}
1063
+ 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"
1064
+ placeholder="Adresse complète de l'entité"
1065
+ />
1066
+ </div>
1067
+ </div>
1068
+ );
1069
+
1070
+ case 'banking':
1071
+ return (
1072
+ <div className="space-y-4">
1073
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1074
+ <TextInput
1075
+ label="RIB"
1076
+ name="rib"
1077
+ value={formData.rib || ''}
1078
+ placeholder="Relevé d'Identité Bancaire"
1079
+ onChange={handleInputChange}
1080
+ />
1081
+
1082
+ <TextInput
1083
+ label="IBAN"
1084
+ name="iban"
1085
+ value={formData.iban || ''}
1086
+ placeholder="FR76 1234 5678 9012 3456 789"
1087
+ error={errors.iban}
1088
+ onChange={handleInputChange}
1089
+ />
1090
+ </div>
1091
+
1092
+ <TextInput
1093
+ label="Nom de la banque"
1094
+ name="bank_name"
1095
+ value={formData.bank_name || ''}
1096
+ placeholder="Nom de la banque"
1097
+ onChange={handleInputChange}
1098
+ />
1099
+
1100
+ <div>
1101
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1102
+ Adresse de la banque
1103
+ </label>
1104
+ <textarea
1105
+ name="bank_address"
1106
+ value={formData.bank_address || ''}
1107
+ onChange={handleTextareaChange}
1108
+ rows={2}
1109
+ 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"
1110
+ placeholder="Adresse de la banque"
1111
+ />
1112
+ </div>
1113
+
1114
+ <SelectInput
1115
+ label="Devise"
1116
+ name="currency"
1117
+ value={formData.currency || 'EUR'}
1118
+ options={currencies}
1119
+ onChange={handleSelectChange}
1120
+ />
1121
+ </div>
1122
+ );
1123
+
1124
+ case 'fne':
1125
+ return (
1126
+ <div className="space-y-4">
1127
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1128
+ <TextInput
1129
+ label="Regime Fiscal"
1130
+ name="regime_taxes"
1131
+ value={formData.regime_taxes || ''}
1132
+ placeholder=""
1133
+ onChange={handleInputChange}
1134
+ />
1135
+ <TextInput
1136
+ label="Centre de taxe"
1137
+ name="centers_taxes"
1138
+ value={formData.centers_taxes || ''}
1139
+ placeholder=""
1140
+ onChange={handleInputChange}
1141
+ />
1142
+ <TextInput
1143
+ label="Centre de taxe"
1144
+ name="centers_taxes"
1145
+ value={formData.centers_taxes || ''}
1146
+ placeholder=""
1147
+ onChange={handleInputChange}
1148
+ />
1149
+
1150
+
1151
+ <TextInput
1152
+ label={`Etablissement`}
1153
+ name="establishment"
1154
+ value={formData.establishment ?? ''}
1155
+ onChange={handleInputChange}
1156
+ />
1157
+
1158
+ <TextInput
1159
+ label={`Point de vente`}
1160
+ name="point_of_sale"
1161
+ value={formData.point_of_sale ?? ''}
1162
+ onChange={handleInputChange}
1163
+ />
1164
+
1165
+ <TextInput
1166
+ label={`Clé d'authentification FNE`}
1167
+ name="fne_auth_key"
1168
+ value={formData.fne_auth_key ?? ''}
1169
+ onChange={handleInputChange}
1170
+ />
1171
+
1172
+ <SelectInput
1173
+ label={`STATUT FNE`}
1174
+ name="fne_url"
1175
+ value={formData.fne_url ?? ''}
1176
+ options={[
1177
+ { value: 'test', label: 'test' },
1178
+ { value: 'prod', label: 'production' },
1179
+ ]}
1180
+ onChange={handleInputChange}
1181
+ />
1182
+
1183
+
1184
+ </div>
1185
+ </div>
1186
+ );
1187
+
1188
+ default:
1189
+ return null;
1190
+ }
1191
+ };
1192
+
1193
+ if (!isOpen) return null;
1194
+
1195
+ return (
1196
+ <Modal
1197
+ title="Créer une entité"
1198
+ description={`la création de l'entité pour l'organisation ${organization?.legal_name}.`}
1199
+ open={isOpen}
1200
+ onClose={onClose}
1201
+ >
1202
+
1203
+
1204
+
1205
+ {/* Tabs */}
1206
+ <div className="border-b border-gray-200">
1207
+ <nav className="flex space-x-8 px-6">
1208
+ {tabs.map((tab) => {
1209
+ const Icon = tab.icon;
1210
+ return (
1211
+ <button
1212
+ type='button'
1213
+ key={tab.id}
1214
+ onClick={() => setActiveTab(tab.id as any)}
1215
+ className={`
1216
+ flex items-center space-x-2 py-4 px-2 text-sm font-medium border-b-2 transition-colors
1217
+ ${activeTab === tab.id
1218
+ ? 'border-[#6A8A82] text-[#6A8A82]'
1219
+ : 'border-transparent text-[#767676] hover:text-[#6A8A82] hover:border-[#6A8A82]/30'
1220
+ }
1221
+ `}
1222
+ >
1223
+ <Icon className="w-4 h-4 inline mr-2" />
1224
+ <span>{tab.label}</span>
1225
+ {tab.badge && (
1226
+ <span className="ml-2 px-2 py-1 text-xs bg-red-100 text-red-600 rounded-full">
1227
+ {tab.badge}
1228
+ </span>
1229
+ )}
1230
+ </button>
1231
+ );
1232
+ })}
1233
+ </nav>
1234
+ </div>
1235
+
1236
+ {/* Content */}
1237
+ <form onSubmit={handleSubmit} className="p-6">
1238
+ {renderTabContent()}
1239
+
1240
+ {/* Actions */}
1241
+ <div className="flex justify-between pt-6 border-t border-gray-200 mt-8">
1242
+ <button
1243
+ type="button"
1244
+ onClick={onClose}
1245
+ className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
1246
+ >
1247
+ Annuler
1248
+ </button>
1249
+
1250
+
1251
+
1252
+ <PrimaryButton
1253
+ type="submit"
1254
+ disabled={loading}
1255
+ >
1256
+
1257
+ {loading ? 'chargement...' : 'Enregistrer l\'entité'}
1258
+ </PrimaryButton>
1259
+ </div>
1260
+ </form>
1261
+
1262
+
1263
+
1264
+
1265
+
1266
+ </Modal>
1267
+
1268
+ );
1269
+ };
1270
+
1271
+ // ModulePurchaseModal Component
1272
+ interface ModulePurchaseModalProps {
1273
+ module: Module;
1274
+ organization: Organization | null;
1275
+ isOpen: boolean;
1276
+ onClose: () => void;
1277
+ onConfirm: () => void;
1278
+ processing: boolean;
1279
+ }
1280
+
1281
+ const ModulePurchaseModal: React.FC<ModulePurchaseModalProps> = ({
1282
+ module,
1283
+ organization,
1284
+ isOpen,
1285
+ onClose,
1286
+ onConfirm,
1287
+ processing
1288
+ }) => {
1289
+ const calculateModulePrice = (module: Module, withTax: boolean = true): number => {
1290
+ const basePrice = module.price_monthly;
1291
+ return withTax ? basePrice * 1.18 : basePrice; // TVA 18%
1292
+ };
1293
+
1294
+ const formatPrice = (price: number): string => {
1295
+ return new Intl.NumberFormat('fr-FR', {
1296
+ minimumFractionDigits: 0,
1297
+ maximumFractionDigits: 0,
1298
+ }).format(price);
1299
+ };
1300
+
1301
+ if (!isOpen) return null;
1302
+
1303
+ const basePrice = module.price_monthly;
1304
+ const tax = basePrice * 0.18;
1305
+ const totalPrice = calculateModulePrice(module, true);
1306
+
1307
+ return (
1308
+ <Modal
1309
+ title="Achat de Module"
1310
+ description={`Confirmez l'achat du module ${module.name} pour l'organisation ${organization?.legal_name}.`}
1311
+ open={isOpen}
1312
+ onClose={onClose}
1313
+ >
1314
+
1315
+
1316
+ {/* Content */}
1317
+ <div className="p-6">
1318
+ {/* Module Info */}
1319
+ <div className="mb-6">
1320
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">{module.name}</h3>
1321
+ <p className="text-sm text-gray-600 mb-4">{module.description}</p>
1322
+
1323
+ {/* Module Features */}
1324
+ <div className="bg-gray-50 rounded-lg p-4 mb-4">
1325
+ <h4 className="text-sm font-medium text-gray-700 mb-2">Fonctionnalités incluses:</h4>
1326
+ <ul className="text-sm text-gray-600 space-y-1">
1327
+ <li>• Accès complet au module {module.name}</li>
1328
+ <li>• Intégration avec les modules existants</li>
1329
+ <li>• Support technique inclus</li>
1330
+ <li>• Mises à jour automatiques</li>
1331
+ </ul>
1332
+ </div>
1333
+ </div>
1334
+
1335
+ {/* Price Breakdown */}
1336
+ <div className="bg-gray-50 rounded-lg p-4 mb-6">
1337
+ <h4 className="text-sm font-semibold text-gray-700 mb-3">Détail du prix:</h4>
1338
+ <div className="space-y-2">
1339
+ <div className="flex justify-between text-sm">
1340
+ <span className="text-gray-600">Prix du module:</span>
1341
+ <span className="font-medium">{formatPrice(basePrice)} FCFA</span>
1342
+ </div>
1343
+ <div className="flex justify-between text-sm">
1344
+ <span className="text-gray-600">TVA (18%):</span>
1345
+ <span className="font-medium">{formatPrice(tax)} FCFA</span>
1346
+ </div>
1347
+ <div className="border-t border-gray-200 pt-2 mt-2">
1348
+ <div className="flex justify-between text-base font-semibold">
1349
+ <span className="text-gray-900">Total:</span>
1350
+ <span className="text-[#8290A9]">{formatPrice(totalPrice)} FCFA/mois</span>
1351
+ </div>
1352
+ </div>
1353
+ </div>
1354
+ </div>
1355
+
1356
+ {/* Warning */}
1357
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
1358
+ <div className="flex">
1359
+ <div className="ml-3">
1360
+ <h5 className="text-sm font-medium text-blue-800">Information</h5>
1361
+ <p className="text-sm text-blue-700 mt-1">
1362
+ Ce module sera ajouté à votre abonnement actuel et facturé mensuellement.
1363
+ </p>
1364
+ </div>
1365
+ </div>
1366
+ </div>
1367
+ </div>
1368
+
1369
+ {/* Actions */}
1370
+ <div className="flex justify-between p-6 border-t border-gray-200">
1371
+ <button
1372
+ type="button"
1373
+ onClick={onClose}
1374
+ disabled={processing}
1375
+ className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1376
+ >
1377
+ Annuler
1378
+ </button>
1379
+
1380
+ <button
1381
+ type="button"
1382
+ onClick={onConfirm}
1383
+ disabled={processing}
1384
+ 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"
1385
+ >
1386
+ {processing ? (
1387
+ <>
1388
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
1389
+ <span>Traitement...</span>
1390
+ </>
1391
+ ) : (
1392
+ <>
1393
+ <CreditCard className="w-4 h-4" />
1394
+ <span>Confirmer l'achat</span>
1395
+ </>
1396
+ )}
1397
+ </button>
1398
+ </div>
1399
+ </Modal>
1400
+ );
1401
+ };
1402
+
1403
+ // PlanChangeModal Component
1404
+ interface PlanChangeModalProps {
1405
+ plans: any[];
1406
+ currentPlan: any;
1407
+ organization: Organization | null;
1408
+ isOpen: boolean;
1409
+ onClose: () => void;
1410
+ onConfirm: (plan: any) => void;
1411
+ processing: boolean;
1412
+ plansLoading: boolean;
1413
+ }
1414
+
1415
+ const PlanChangeModal: React.FC<PlanChangeModalProps> = ({
1416
+ plans,
1417
+ currentPlan,
1418
+ organization,
1419
+ isOpen,
1420
+ onClose,
1421
+ onConfirm,
1422
+ processing,
1423
+ plansLoading
1424
+ }) => {
1425
+ const [selectedPlan, setSelectedPlan] = useState<any>(null);
1426
+
1427
+ const formatPrice = (price: number): string => {
1428
+ return new Intl.NumberFormat('fr-FR', {
1429
+ minimumFractionDigits: 0,
1430
+ maximumFractionDigits: 0,
1431
+ }).format(price);
1432
+ };
1433
+
1434
+ const handlePlanSelect = (plan: any) => {
1435
+ if (plan.id === currentPlan?.id) return; // Ne pas permettre de sélectionner le plan actuel
1436
+ setSelectedPlan(plan);
1437
+ };
1438
+
1439
+ const handleConfirm = () => {
1440
+ if (selectedPlan) {
1441
+ onConfirm(selectedPlan);
1442
+ }
1443
+ };
1444
+
1445
+ if (!isOpen) return null;
1446
+
1447
+ return (
1448
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
1449
+ <div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
1450
+ {/* Header */}
1451
+ <div className="p-6 border-b border-gray-200">
1452
+ <div className="flex items-center justify-between">
1453
+ <div className="flex items-center space-x-3">
1454
+ <div className="w-10 h-10 bg-[#8290A9] rounded-full flex items-center justify-center">
1455
+ <CreditCard className="w-5 h-5 text-white" />
1456
+ </div>
1457
+ <div>
1458
+ <h2 className="text-xl font-semibold text-gray-900">Changer de Plan</h2>
1459
+ <p className="text-sm text-gray-600">
1460
+ Organisation: <span className="font-medium">{organization?.legal_name}</span>
1461
+ </p>
1462
+ </div>
1463
+ </div>
1464
+ <button
1465
+ onClick={onClose}
1466
+ disabled={processing}
1467
+ className="text-gray-400 hover:text-gray-600 transition-colors disabled:opacity-50"
1468
+ >
1469
+ <X className="w-6 h-6" />
1470
+ </button>
1471
+ </div>
1472
+ </div>
1473
+
1474
+ {/* Content */}
1475
+ <div className="p-6">
1476
+ {plansLoading ? (
1477
+ <div className="text-center py-8">
1478
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8290A9] mx-auto mb-4"></div>
1479
+ <p className="text-gray-600">Chargement des plans...</p>
1480
+ </div>
1481
+ ) : (
1482
+ <>
1483
+ {/* Current Plan Info */}
1484
+ <div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
1485
+ <h3 className="text-sm font-medium text-blue-800 mb-1">Plan actuel</h3>
1486
+ <p className="text-sm text-blue-700">
1487
+ <span className="font-semibold">{currentPlan?.name}</span> - {formatPrice(currentPlan?.price_monthly || 0)} FCFA/mois
1488
+ </p>
1489
+ </div>
1490
+
1491
+ {/* Plans Grid */}
1492
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
1493
+ {plans.map((plan) => {
1494
+ const isCurrentPlan = plan.id === currentPlan?.id;
1495
+ const isSelected = selectedPlan?.id === plan.id;
1496
+
1497
+ return (
1498
+ <div
1499
+ key={plan.id}
1500
+ onClick={() => !isCurrentPlan && handlePlanSelect(plan)}
1501
+ className={`relative p-6 rounded-lg border-2 transition-all cursor-pointer ${isCurrentPlan
1502
+ ? 'border-gray-300 bg-gray-50 cursor-not-allowed opacity-75'
1503
+ : isSelected
1504
+ ? 'border-[#8290A9] bg-[#8290A9]/5'
1505
+ : 'border-gray-200 hover:border-[#8290A9]/50'
1506
+ }`}
1507
+ >
1508
+ {/* Popular Badge */}
1509
+ {plan.popular && (
1510
+ <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
1511
+ <span className="bg-teal-100 text-teal-700 px-3 py-1 rounded-full text-xs font-medium">
1512
+ Populaire
1513
+ </span>
1514
+ </div>
1515
+ )}
1516
+
1517
+ {/* Current Plan Badge */}
1518
+ {isCurrentPlan && (
1519
+ <div className="absolute -top-3 right-4">
1520
+ <span className="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-xs font-medium">
1521
+ Plan actuel
1522
+ </span>
1523
+ </div>
1524
+ )}
1525
+
1526
+ {/* Selected indicator */}
1527
+ {isSelected && (
1528
+ <div className="absolute top-4 right-4">
1529
+ <div className="w-6 h-6 bg-[#8290A9] rounded-full flex items-center justify-center">
1530
+ <div className="w-2 h-2 bg-white rounded-full"></div>
1531
+ </div>
1532
+ </div>
1533
+ )}
1534
+
1535
+ {/* Plan Content */}
1536
+ <div className="mb-4">
1537
+ <h3 className="text-xl font-bold text-gray-900 mb-2">{plan.name}</h3>
1538
+ <div className="mb-3">
1539
+ <span className="text-2xl font-bold text-gray-900">
1540
+ {formatPrice(plan.price)} FCFA
1541
+ </span>
1542
+ <span className="text-gray-600 ml-1">{plan.period}</span>
1543
+ {plan.originalPrice && (
1544
+ <div className="text-sm text-gray-500">
1545
+ {formatPrice(plan.originalPrice)} FCFA lorsque facturé annuellement
1546
+ </div>
1547
+ )}
1548
+ </div>
1549
+ <p className="text-gray-600 text-sm">{plan.description}</p>
1550
+ </div>
1551
+
1552
+ {/* Features */}
1553
+ <ul className="space-y-2">
1554
+ {plan.features.slice(0, 3).map((feature: any, index: number) => (
1555
+ <li key={index} className="flex items-center space-x-2 text-sm">
1556
+ <div className="w-4 h-4 rounded-full bg-green-100 flex items-center justify-center">
1557
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
1558
+ </div>
1559
+ <span className="text-gray-700">{feature.name}</span>
1560
+ </li>
1561
+ ))}
1562
+ {plan.features.length > 3 && (
1563
+ <li className="text-sm text-gray-500 ml-6">
1564
+ +{plan.features.length - 3} autres fonctionnalités
1565
+ </li>
1566
+ )}
1567
+ </ul>
1568
+ </div>
1569
+ );
1570
+ })}
1571
+ </div>
1572
+
1573
+ {/* Warning */}
1574
+ {selectedPlan && (
1575
+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
1576
+ <div className="flex">
1577
+ <div className="ml-3">
1578
+ <h5 className="text-sm font-medium text-yellow-800">Changement de plan</h5>
1579
+ <p className="text-sm text-yellow-700 mt-1">
1580
+ Le changement de plan prendra effet immédiatement.
1581
+ {selectedPlan.price > currentPlan?.price_monthly && (
1582
+ " La différence de prix sera facturée au prorata."
1583
+ )}
1584
+ {selectedPlan.price < currentPlan?.price_monthly && (
1585
+ " Le crédit restant sera appliqué à votre prochaine facture."
1586
+ )}
1587
+ </p>
1588
+ </div>
1589
+ </div>
1590
+ </div>
1591
+ )}
1592
+ </>
1593
+ )}
1594
+ </div>
1595
+
1596
+ {/* Actions */}
1597
+ <div className="flex justify-between p-6 border-t border-gray-200">
1598
+ <button
1599
+ type="button"
1600
+ onClick={onClose}
1601
+ disabled={processing}
1602
+ className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1603
+ >
1604
+ Annuler
1605
+ </button>
1606
+
1607
+ <button
1608
+ type="button"
1609
+ onClick={handleConfirm}
1610
+ disabled={processing || !selectedPlan || plansLoading}
1611
+ 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"
1612
+ >
1613
+ {processing ? (
1614
+ <>
1615
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
1616
+ <span>Traitement...</span>
1617
+ </>
1618
+ ) : (
1619
+ <>
1620
+ <CreditCard className="w-4 h-4" />
1621
+ <span>Confirmer le changement</span>
1622
+ </>
1623
+ )}
1624
+ </button>
1625
+ </div>
1626
+ </div>
1627
+ </div>
1628
+ );
1629
+ };