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