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,450 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
ArrowLeft,
|
|
5
|
+
User as UserIcon,
|
|
6
|
+
Mail,
|
|
7
|
+
Phone,
|
|
8
|
+
Shield,
|
|
9
|
+
Users,
|
|
10
|
+
Save,
|
|
11
|
+
X
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { useToast } from '../../contexts/ToastContext';
|
|
14
|
+
import { useSession } from '../../contexts/SessionContext';
|
|
15
|
+
import { UserServices } from '../../services/UserServices';
|
|
16
|
+
import { User } from '../../models/User';
|
|
17
|
+
import { RewiseBasicCard } from '../../components/common/Cards';
|
|
18
|
+
import { TextInput, InputField, SelectInput } from '../../components/common/Inputs';
|
|
19
|
+
|
|
20
|
+
const CreateUser: React.FC = () => {
|
|
21
|
+
const [formData, setFormData] = useState<Partial<User>>({
|
|
22
|
+
username: '',
|
|
23
|
+
first_name: '',
|
|
24
|
+
last_name: '',
|
|
25
|
+
email: '',
|
|
26
|
+
phonenumber: '',
|
|
27
|
+
is_active: true,
|
|
28
|
+
is_staff: false,
|
|
29
|
+
is_superuser: false,
|
|
30
|
+
groups: [],
|
|
31
|
+
user_permissions: [],
|
|
32
|
+
centers_access: []
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const [errors, setErrors] = useState<Partial<Record<keyof User, string>>>({});
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const [activeTab, setActiveTab] = useState<'general' | 'permissions' | 'access'>('general');
|
|
38
|
+
|
|
39
|
+
const navigate = useNavigate();
|
|
40
|
+
const { success, error: showError } = useToast();
|
|
41
|
+
const { token } = useSession();
|
|
42
|
+
|
|
43
|
+
const tabs = [
|
|
44
|
+
{ id: 'general', label: 'Informations générales', icon: UserIcon },
|
|
45
|
+
{ id: 'permissions', label: 'Permissions', icon: Shield },
|
|
46
|
+
{ id: 'permissions', label: 'Permissions', icon: Shield },
|
|
47
|
+
{ id: 'access', label: 'Accès aux centres', icon: Users }
|
|
48
|
+
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
52
|
+
const { name, value, type, checked } = e.target;
|
|
53
|
+
setFormData(prev => ({
|
|
54
|
+
...prev,
|
|
55
|
+
[name]: type === 'checkbox' ? checked : value
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
if (errors[name as keyof User]) {
|
|
59
|
+
setErrors(prev => ({ ...prev, [name]: undefined }));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
64
|
+
const { name, value } = e.target;
|
|
65
|
+
setFormData(prev => ({ ...prev, [name]: value === 'true' }));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const validateForm = (): boolean => {
|
|
69
|
+
const newErrors: Partial<Record<keyof User, string>> = {};
|
|
70
|
+
|
|
71
|
+
if (!formData.username?.trim()) {
|
|
72
|
+
newErrors.username = 'Le nom d\'utilisateur est obligatoire';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!formData.first_name?.trim()) {
|
|
76
|
+
newErrors.first_name = 'Le pr�nom est obligatoire';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!formData.last_name?.trim()) {
|
|
80
|
+
newErrors.last_name = 'Le nom de famille est obligatoire';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!formData.email?.trim()) {
|
|
84
|
+
newErrors.email = 'L\'email est obligatoire';
|
|
85
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
86
|
+
newErrors.email = 'Format d\'email invalide';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (formData.phonenumber && !/^[\+]?[0-9\s\-\(\)]+$/.test(formData.phonenumber)) {
|
|
90
|
+
newErrors.phonenumber = 'Format de t�l�phone invalide';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setErrors(newErrors);
|
|
94
|
+
return Object.keys(newErrors).length === 0;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
if (!validateForm()) return;
|
|
100
|
+
|
|
101
|
+
if (!token) {
|
|
102
|
+
showError('Vous devez �tre connect� pour cr�er un utilisateur');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setLoading(true);
|
|
107
|
+
try {
|
|
108
|
+
await UserServices.addUser(formData, token);
|
|
109
|
+
success('Utilisateur cr�� avec succ�s !');
|
|
110
|
+
navigate('/users');
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
console.error(error);
|
|
113
|
+
showError('Erreur lors de la cr�ation de l\'utilisateur');
|
|
114
|
+
} finally {
|
|
115
|
+
setLoading(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const addCenterAccess = () => {
|
|
120
|
+
setFormData(prev => ({
|
|
121
|
+
...prev,
|
|
122
|
+
centers_access: [
|
|
123
|
+
...(prev.centers_access || []),
|
|
124
|
+
{ id: 0, permissions: [] }
|
|
125
|
+
]
|
|
126
|
+
}));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const removeCenterAccess = (index: number) => {
|
|
130
|
+
setFormData(prev => ({
|
|
131
|
+
...prev,
|
|
132
|
+
centers_access: prev.centers_access?.filter((_, i) => i !== index) || []
|
|
133
|
+
}));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const updateCenterAccess = (index: number, field: 'id' | 'permissions', value: any) => {
|
|
137
|
+
setFormData(prev => ({
|
|
138
|
+
...prev,
|
|
139
|
+
centers_access: prev.centers_access?.map((center, i) =>
|
|
140
|
+
i === index ? { ...center, [field]: value } : center
|
|
141
|
+
) || []
|
|
142
|
+
}));
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const renderTabContent = () => {
|
|
146
|
+
switch (activeTab) {
|
|
147
|
+
case 'general':
|
|
148
|
+
return (
|
|
149
|
+
<div className="space-y-6">
|
|
150
|
+
{/* Informations de base */}
|
|
151
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
152
|
+
<TextInput
|
|
153
|
+
label="Nom d'utilisateur"
|
|
154
|
+
name="username"
|
|
155
|
+
value={formData.username || ''}
|
|
156
|
+
placeholder="nom utilisateur"
|
|
157
|
+
required
|
|
158
|
+
error={errors.username}
|
|
159
|
+
onChange={handleInputChange}
|
|
160
|
+
/>
|
|
161
|
+
|
|
162
|
+
<InputField
|
|
163
|
+
label="Email"
|
|
164
|
+
name="email"
|
|
165
|
+
type="email"
|
|
166
|
+
value={formData.email || ''}
|
|
167
|
+
placeholder="utilisateur@example.com"
|
|
168
|
+
required
|
|
169
|
+
error={errors.email}
|
|
170
|
+
onChange={handleInputChange}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
175
|
+
<TextInput
|
|
176
|
+
label="Pr�nom"
|
|
177
|
+
name="first_name"
|
|
178
|
+
value={formData.first_name || ''}
|
|
179
|
+
placeholder="Pr�nom"
|
|
180
|
+
required
|
|
181
|
+
error={errors.first_name}
|
|
182
|
+
onChange={handleInputChange}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<TextInput
|
|
186
|
+
label="Nom de famille"
|
|
187
|
+
name="last_name"
|
|
188
|
+
value={formData.last_name || ''}
|
|
189
|
+
placeholder="Nom de famille"
|
|
190
|
+
required
|
|
191
|
+
error={errors.last_name}
|
|
192
|
+
onChange={handleInputChange}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
197
|
+
<InputField
|
|
198
|
+
label="T�l�phone"
|
|
199
|
+
name="phonenumber"
|
|
200
|
+
type="tel"
|
|
201
|
+
value={formData.phonenumber || ''}
|
|
202
|
+
placeholder="+225 XX XX XXX XXX"
|
|
203
|
+
error={errors.phonenumber}
|
|
204
|
+
onChange={handleInputChange}
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
<SelectInput
|
|
208
|
+
label="Statut"
|
|
209
|
+
name="is_active"
|
|
210
|
+
value={formData.is_active?.toString() || 'true'}
|
|
211
|
+
options={[
|
|
212
|
+
{ value: 'true', label: 'Actif' },
|
|
213
|
+
{ value: 'false', label: 'Inactif' }
|
|
214
|
+
]}
|
|
215
|
+
onChange={handleSelectChange}
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
case 'permissions':
|
|
222
|
+
return (
|
|
223
|
+
<div className="space-y-6">
|
|
224
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
225
|
+
<div className="space-y-4">
|
|
226
|
+
<label className="flex items-center space-x-3">
|
|
227
|
+
<input
|
|
228
|
+
type="checkbox"
|
|
229
|
+
name="is_staff"
|
|
230
|
+
checked={formData.is_staff || false}
|
|
231
|
+
onChange={handleInputChange}
|
|
232
|
+
className="rounded border-gray-300 text-[#8290A9] focus:ring-[#8290A9]"
|
|
233
|
+
/>
|
|
234
|
+
<div>
|
|
235
|
+
<span className="text-sm font-medium text-gray-900">Membre du personnel</span>
|
|
236
|
+
<p className="text-xs text-gray-500">
|
|
237
|
+
Permet d'acc�der � l'interface d'administration
|
|
238
|
+
</p>
|
|
239
|
+
</div>
|
|
240
|
+
</label>
|
|
241
|
+
|
|
242
|
+
<label className="flex items-center space-x-3">
|
|
243
|
+
<input
|
|
244
|
+
type="checkbox"
|
|
245
|
+
name="is_superuser"
|
|
246
|
+
checked={formData.is_superuser || false}
|
|
247
|
+
onChange={handleInputChange}
|
|
248
|
+
className="rounded border-gray-300 text-[#8290A9] focus:ring-[#8290A9]"
|
|
249
|
+
/>
|
|
250
|
+
<div>
|
|
251
|
+
<span className="text-sm font-medium text-gray-900">Super utilisateur</span>
|
|
252
|
+
<p className="text-xs text-gray-500">
|
|
253
|
+
Acc�s complet � toutes les fonctionnalit�s
|
|
254
|
+
</p>
|
|
255
|
+
</div>
|
|
256
|
+
</label>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
|
261
|
+
<h4 className="text-sm font-medium text-gray-700 mb-2">Groupes d'utilisateurs</h4>
|
|
262
|
+
<p className="text-xs text-gray-500 mb-4">
|
|
263
|
+
Les groupes seront configurables apr�s la cr�ation de l'utilisateur
|
|
264
|
+
</p>
|
|
265
|
+
<div className="text-sm text-gray-600">
|
|
266
|
+
Groupes assign�s : {formData.groups?.length || 0}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
case 'access':
|
|
273
|
+
return (
|
|
274
|
+
<div className="space-y-6">
|
|
275
|
+
<div className="flex justify-between items-center">
|
|
276
|
+
<div>
|
|
277
|
+
<h4 className="text-sm font-medium text-gray-700">Acc�s aux centres</h4>
|
|
278
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
279
|
+
Configurez l'acc�s de l'utilisateur aux diff�rents centres
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={addCenterAccess}
|
|
285
|
+
className="bg-[#8290A9] text-white px-3 py-1 rounded text-sm hover:bg-[#6B7C92] transition-colors"
|
|
286
|
+
>
|
|
287
|
+
Ajouter un centre
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{formData.centers_access && formData.centers_access.length > 0 ? (
|
|
292
|
+
<div className="space-y-4">
|
|
293
|
+
{formData.centers_access.map((center, index) => (
|
|
294
|
+
<div key={index} className="border border-gray-200 rounded-lg p-4">
|
|
295
|
+
<div className="flex justify-between items-start mb-4">
|
|
296
|
+
<h5 className="text-sm font-medium text-gray-700">
|
|
297
|
+
Centre d'acc�s #{index + 1}
|
|
298
|
+
</h5>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={() => removeCenterAccess(index)}
|
|
302
|
+
className="text-red-600 hover:text-red-800"
|
|
303
|
+
>
|
|
304
|
+
<X className="w-4 h-4" />
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
309
|
+
<div>
|
|
310
|
+
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
311
|
+
ID du centre
|
|
312
|
+
</label>
|
|
313
|
+
<input
|
|
314
|
+
type="number"
|
|
315
|
+
value={center.id}
|
|
316
|
+
onChange={(e) => updateCenterAccess(index, 'id', parseInt(e.target.value) || 0)}
|
|
317
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
318
|
+
placeholder="ID du centre"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div>
|
|
323
|
+
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
324
|
+
Permissions (IDs s�par�s par des virgules)
|
|
325
|
+
</label>
|
|
326
|
+
<input
|
|
327
|
+
type="text"
|
|
328
|
+
value={center.permissions?.join(', ') || ''}
|
|
329
|
+
onChange={(e) => {
|
|
330
|
+
const permissions = e.target.value
|
|
331
|
+
.split(',')
|
|
332
|
+
.map(p => parseInt(p.trim()))
|
|
333
|
+
.filter(p => !isNaN(p));
|
|
334
|
+
updateCenterAccess(index, 'permissions', permissions);
|
|
335
|
+
}}
|
|
336
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
337
|
+
placeholder="1, 2, 3"
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
) : (
|
|
345
|
+
<div className="text-center py-8 bg-gray-50 rounded-lg">
|
|
346
|
+
<Users className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
|
347
|
+
<p className="text-gray-500 text-sm">Aucun acc�s aux centres configur�</p>
|
|
348
|
+
<p className="text-gray-400 text-xs mt-1">
|
|
349
|
+
Cliquez sur "Ajouter un centre" pour commencer
|
|
350
|
+
</p>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
default:
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div className="space-y-6">
|
|
363
|
+
{/* Header */}
|
|
364
|
+
<div className="flex items-center justify-between">
|
|
365
|
+
<div className="flex items-center space-x-4">
|
|
366
|
+
<button
|
|
367
|
+
onClick={() => navigate('/users')}
|
|
368
|
+
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors"
|
|
369
|
+
>
|
|
370
|
+
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
371
|
+
Retour aux utilisateurs
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{/* Form */}
|
|
377
|
+
<RewiseBasicCard title={
|
|
378
|
+
<div className="flex items-center space-x-3">
|
|
379
|
+
<div className="w-10 h-10 bg-[#8290A9] rounded-full flex items-center justify-center">
|
|
380
|
+
<UserIcon className="w-5 h-5 text-white" />
|
|
381
|
+
</div>
|
|
382
|
+
<div>
|
|
383
|
+
<h1 className="text-xl font-semibold text-gray-900">Cr�er un utilisateur</h1>
|
|
384
|
+
<p className="text-sm text-gray-600">Ajoutez un nouveau membre � votre �quipe</p>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
}>
|
|
388
|
+
<form onSubmit={handleSubmit}>
|
|
389
|
+
{/* Tabs */}
|
|
390
|
+
<div className="border-b border-gray-200 mb-6">
|
|
391
|
+
<nav className="flex space-x-8">
|
|
392
|
+
{tabs.map((tab) => {
|
|
393
|
+
const Icon = tab.icon;
|
|
394
|
+
return (
|
|
395
|
+
<button
|
|
396
|
+
key={tab.id}
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => setActiveTab(tab.id as any)}
|
|
399
|
+
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
400
|
+
activeTab === tab.id
|
|
401
|
+
? 'border-[#8290A9] text-[#8290A9]'
|
|
402
|
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
403
|
+
}`}
|
|
404
|
+
>
|
|
405
|
+
<Icon className="w-4 h-4 inline mr-2" />
|
|
406
|
+
{tab.label}
|
|
407
|
+
</button>
|
|
408
|
+
);
|
|
409
|
+
})}
|
|
410
|
+
</nav>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{/* Tab Content */}
|
|
414
|
+
{renderTabContent()}
|
|
415
|
+
|
|
416
|
+
{/* Actions */}
|
|
417
|
+
<div className="flex justify-between pt-6 border-t border-gray-200 mt-8">
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
onClick={() => navigate('/users')}
|
|
421
|
+
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
422
|
+
>
|
|
423
|
+
Annuler
|
|
424
|
+
</button>
|
|
425
|
+
|
|
426
|
+
<button
|
|
427
|
+
type="submit"
|
|
428
|
+
disabled={loading}
|
|
429
|
+
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"
|
|
430
|
+
>
|
|
431
|
+
{loading ? (
|
|
432
|
+
<>
|
|
433
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
434
|
+
<span>Cr�ation...</span>
|
|
435
|
+
</>
|
|
436
|
+
) : (
|
|
437
|
+
<>
|
|
438
|
+
<Save className="w-4 h-4" />
|
|
439
|
+
<span>Cr�er l'utilisateur</span>
|
|
440
|
+
</>
|
|
441
|
+
)}
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
</form>
|
|
445
|
+
</RewiseBasicCard>
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export default CreateUser;
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ThermometerSnowflake } from "lucide-react";
|
|
2
|
+
import { API_URL } from "./api";
|
|
3
|
+
import { User } from "../models/User";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const API_BASE_URL = `${API_URL}/core/auth/`;
|
|
7
|
+
|
|
8
|
+
export interface SendOtpPayload {
|
|
9
|
+
email: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface VerifyOtpPayload {
|
|
13
|
+
email: string;
|
|
14
|
+
otp: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CompleteRegistrationPayload {
|
|
18
|
+
email: string;
|
|
19
|
+
password: string;
|
|
20
|
+
confirmed_password: string;
|
|
21
|
+
firstname?: string;
|
|
22
|
+
lastname?: string;
|
|
23
|
+
phonenumber?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AddUserPayload {
|
|
27
|
+
email: string;
|
|
28
|
+
first_name: string;
|
|
29
|
+
last_name: string;
|
|
30
|
+
phonenumber?: string;
|
|
31
|
+
centers_access?: Array<{
|
|
32
|
+
id: number;
|
|
33
|
+
permissions?: number[];
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LoginPayload {
|
|
38
|
+
username: string;
|
|
39
|
+
password: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class FetchApi {
|
|
43
|
+
static async post<T>(url: string, payload?: any, token?: string): Promise<T> {
|
|
44
|
+
const headers: Record<string, string> = {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
};
|
|
47
|
+
if (token) {
|
|
48
|
+
headers["Authorization"] = `Token ${token}`;
|
|
49
|
+
}
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers,
|
|
53
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) throw new Error(await res.text());
|
|
56
|
+
return res.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async get<T>(url: string, token?: string): Promise<T> {
|
|
60
|
+
const headers: Record<string, string> = {};
|
|
61
|
+
if (token) {
|
|
62
|
+
headers["Authorization"] = `Token ${token}`;
|
|
63
|
+
}
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
headers,
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(await res.text());
|
|
69
|
+
return res.json();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const AuthServices = {
|
|
74
|
+
sendOtp: (payload: SendOtpPayload) =>
|
|
75
|
+
FetchApi.post(`${API_BASE_URL}send-otp/`, payload),
|
|
76
|
+
|
|
77
|
+
verifyOtp: (payload: VerifyOtpPayload) =>
|
|
78
|
+
FetchApi.post(`${API_BASE_URL}verify-otp/`, payload),
|
|
79
|
+
|
|
80
|
+
completeRegistration: (payload: CompleteRegistrationPayload) =>
|
|
81
|
+
FetchApi.post(`${API_BASE_URL}complete-registration/`, payload),
|
|
82
|
+
|
|
83
|
+
addUser: (payload: User) =>
|
|
84
|
+
FetchApi.post(`${API_BASE_URL}add-user/`, payload),
|
|
85
|
+
|
|
86
|
+
login: (payload: LoginPayload) =>
|
|
87
|
+
FetchApi.post(`${API_BASE_URL}login/`, payload),
|
|
88
|
+
|
|
89
|
+
getUserInformations: (token: string) =>
|
|
90
|
+
FetchApi.get(`${API_BASE_URL}user-informations/`, token),
|
|
91
|
+
|
|
92
|
+
logout: () =>
|
|
93
|
+
FetchApi.post(`${API_BASE_URL}logout/`),
|
|
94
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { sub } from 'date-fns';
|
|
4
|
+
import { API_URL, FetchApi } from './api';
|
|
5
|
+
import { Entity, Organization } from '../models/Organization';
|
|
6
|
+
|
|
7
|
+
const ORGANIZATIONS_API_URL = `${API_URL}/core/organizations/`;
|
|
8
|
+
const ENTITIES_API_URL = `${API_URL}/core/entities/`;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
interface SubscriptionData {
|
|
13
|
+
plan: number;
|
|
14
|
+
billing_cycle: 'monthly' | 'yearly';
|
|
15
|
+
extra_modules?: number[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const OrganizationServices = {
|
|
19
|
+
createOrganization: (data: Partial<Organization>, token: string) =>
|
|
20
|
+
FetchApi.post(`${ORGANIZATIONS_API_URL}`, data, token),
|
|
21
|
+
|
|
22
|
+
getUserOrganizations: (token: string) =>
|
|
23
|
+
FetchApi.get(`${API_URL}/core/users/organizations/`, token),
|
|
24
|
+
|
|
25
|
+
getOrganization: (id: number, token: string) =>
|
|
26
|
+
FetchApi.get(`${ORGANIZATIONS_API_URL}${id}/`, token),
|
|
27
|
+
|
|
28
|
+
updateOrganization: (id: number, data: Partial<Organization>, token: string) =>
|
|
29
|
+
FetchApi.put(`${ORGANIZATIONS_API_URL}${id}/`, data, token),
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
subscribe: (id: number, data: SubscriptionData, token: string) =>
|
|
33
|
+
FetchApi.put(`${ORGANIZATIONS_API_URL}${id}/subscribe/`, data, token),
|
|
34
|
+
|
|
35
|
+
deleteOrganization: (id: number, token: string) =>
|
|
36
|
+
FetchApi.delete(`${ORGANIZATIONS_API_URL}${id}/`, token),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const EntityServices = {
|
|
40
|
+
createEntity: (organizationId: number, data: Entity, token: string) =>
|
|
41
|
+
FetchApi.post(`${ENTITIES_API_URL}`, { ...data, organization: organizationId }, token),
|
|
42
|
+
|
|
43
|
+
getOrganizationEntities: (organizationId: number, token: string) =>
|
|
44
|
+
FetchApi.get(`${ORGANIZATIONS_API_URL}${organizationId}/entities/`, token),
|
|
45
|
+
|
|
46
|
+
getEntity: (id: number, token: string) =>
|
|
47
|
+
FetchApi.get(`${ENTITIES_API_URL}${id}/`, token),
|
|
48
|
+
|
|
49
|
+
updateEntity: (id: number, data: Partial<Entity>, token: string) =>
|
|
50
|
+
FetchApi.put(`${ENTITIES_API_URL}${id}/`, data, token),
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
getEntityUsers: (id: number, token: string) =>
|
|
54
|
+
FetchApi.get(`${ENTITIES_API_URL}${id}/users/`, token),
|
|
55
|
+
|
|
56
|
+
addUserToEntity: (entityId: number, userId: number, token: string) =>
|
|
57
|
+
FetchApi.post(`${ENTITIES_API_URL}${entityId}/users/`, { user_id: userId }, token),
|
|
58
|
+
|
|
59
|
+
deleteEntity: (id: number, token: string) =>
|
|
60
|
+
FetchApi.delete(`${ENTITIES_API_URL}${id}/`, token),
|
|
61
|
+
};
|