ptechcore_ui 1.0.1 → 1.0.2

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