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,101 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BarChart3, TrendingUp, Users, Clock } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
const Analytics: React.FC = () => {
|
|
5
|
+
const stats = [
|
|
6
|
+
{
|
|
7
|
+
title: 'Performances équipe',
|
|
8
|
+
value: '94%',
|
|
9
|
+
change: '+12%',
|
|
10
|
+
trend: 'up',
|
|
11
|
+
icon: TrendingUp,
|
|
12
|
+
color: 'text-green-600'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
title: 'Membres actifs',
|
|
16
|
+
value: '24',
|
|
17
|
+
change: '+3',
|
|
18
|
+
trend: 'up',
|
|
19
|
+
icon: Users,
|
|
20
|
+
color: 'text-blue-600'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'Temps moyen',
|
|
24
|
+
value: '2.4h',
|
|
25
|
+
change: '-0.3h',
|
|
26
|
+
trend: 'down',
|
|
27
|
+
icon: Clock,
|
|
28
|
+
color: 'text-purple-600'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'Tâches complétées',
|
|
32
|
+
value: '187',
|
|
33
|
+
change: '+23',
|
|
34
|
+
trend: 'up',
|
|
35
|
+
icon: BarChart3,
|
|
36
|
+
color: 'text-orange-600'
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-6">
|
|
42
|
+
{/* Header */}
|
|
43
|
+
<div>
|
|
44
|
+
<h1 className="text-3xl font-bold text-gray-900">Analytics</h1>
|
|
45
|
+
<p className="text-gray-600 mt-2">
|
|
46
|
+
Analysez les performances de votre équipe et suivez les métriques importantes
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Stats Grid */}
|
|
51
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
52
|
+
{stats.map((stat, index) => {
|
|
53
|
+
const Icon = stat.icon;
|
|
54
|
+
return (
|
|
55
|
+
<div key={index} className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
56
|
+
<div className="flex items-center justify-between">
|
|
57
|
+
<div>
|
|
58
|
+
<p className="text-sm font-medium text-gray-600">{stat.title}</p>
|
|
59
|
+
<p className="text-2xl font-bold text-gray-900 mt-2">{stat.value}</p>
|
|
60
|
+
<p className={`text-sm mt-2 ${
|
|
61
|
+
stat.trend === 'up' ? 'text-green-600' : 'text-red-600'
|
|
62
|
+
}`}>
|
|
63
|
+
{stat.change} vs mois dernier
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div className={`p-3 rounded-full bg-gray-50 ${stat.color}`}>
|
|
67
|
+
<Icon className="w-6 h-6" />
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Charts Section */}
|
|
76
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
77
|
+
{/* Chart 1 */}
|
|
78
|
+
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
79
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
80
|
+
Tendance des performances
|
|
81
|
+
</h3>
|
|
82
|
+
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
|
83
|
+
<p className="text-gray-500">Graphique des performances</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Chart 2 */}
|
|
88
|
+
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
89
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
90
|
+
Répartition des tâches
|
|
91
|
+
</h3>
|
|
92
|
+
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
|
93
|
+
<p className="text-gray-500">Graphique en secteurs</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default Analytics;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { ArrowLeft } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import { Organization, OrganizationServices } from '../services/OrganizationServices';
|
|
7
|
+
import { useSession } from '../contexts/SessionContext';
|
|
8
|
+
import { useToast } from '../contexts/ToastContext';
|
|
9
|
+
import { InputField, TextInput } from '../components/common/Inputs';
|
|
10
|
+
import PrimaryButton from '../components/common/Buttons';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
type Step = 'organization' | 'entity' | 'confirmation';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const CreateOrganization: React.FC = () => {
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const { loggedUser, token } = useSession();
|
|
19
|
+
const { success, error: showError } = useToast();
|
|
20
|
+
|
|
21
|
+
const [formData, setFormData] = useState<Partial<Organization>>({
|
|
22
|
+
legal_name: '',
|
|
23
|
+
trading_name: '',
|
|
24
|
+
phone: '',
|
|
25
|
+
email: '',
|
|
26
|
+
address: '',
|
|
27
|
+
owner: loggedUser?.id || 0,
|
|
28
|
+
created_at: '',
|
|
29
|
+
updated_at: '',
|
|
30
|
+
is_deleted: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const [errors, setErrors] = useState<Partial<Record<keyof Organization, string>>>({});
|
|
34
|
+
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (loggedUser) {
|
|
40
|
+
setFormData(prev => ({ ...prev, owner: loggedUser.id }));
|
|
41
|
+
}
|
|
42
|
+
}, [loggedUser]);
|
|
43
|
+
|
|
44
|
+
const validateForm = (): boolean => {
|
|
45
|
+
const newErrors: Partial<Record<keyof Organization, string>> = {};
|
|
46
|
+
|
|
47
|
+
if (!formData.legal_name?.trim()) {
|
|
48
|
+
newErrors.legal_name = 'La raison sociale est obligatoire';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
52
|
+
newErrors.email = 'Format d\'email invalide';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setErrors(newErrors);
|
|
56
|
+
return Object.keys(newErrors).length === 0;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
60
|
+
const { name, value } = e.target;
|
|
61
|
+
setFormData(prev => ({ ...prev, [name]: value }));
|
|
62
|
+
if (errors[name as keyof Organization]) {
|
|
63
|
+
setErrors(prev => ({ ...prev, [name]: undefined }));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
68
|
+
const { name, value } = e.target;
|
|
69
|
+
setFormData(prev => ({ ...prev, [name]: value }));
|
|
70
|
+
if (errors[name as keyof Organization]) {
|
|
71
|
+
setErrors(prev => ({ ...prev, [name]: undefined }));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
if (!validateForm() || !token) {
|
|
81
|
+
if (!token) {
|
|
82
|
+
showError('Vous devez être connecté pour créer une organisation');
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setLoading(true);
|
|
88
|
+
try {
|
|
89
|
+
await OrganizationServices.createOrganization(formData as Partial<Organization>, token);
|
|
90
|
+
success('Organisation créée avec succès !');
|
|
91
|
+
navigate('/organizations');
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
showError(err.message || 'Erreur lors de la création de l\'organisation');
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="min-h-screen bg-gray-50 py-8">
|
|
105
|
+
<div className=" mx-auto px-4">
|
|
106
|
+
{/* Header */}
|
|
107
|
+
<div className="flex items-center mb-8">
|
|
108
|
+
<button
|
|
109
|
+
onClick={() => navigate('/dashboard')}
|
|
110
|
+
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors"
|
|
111
|
+
>
|
|
112
|
+
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
113
|
+
Retour au tableau de bord
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Title */}
|
|
118
|
+
<div className=" mb-8">
|
|
119
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
|
120
|
+
Création d'organisation
|
|
121
|
+
</h1>
|
|
122
|
+
<p className="text-gray-600">
|
|
123
|
+
Configurez votre organisation et créez votre première entité
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Step Indicator */}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
{/* Current Step Content */}
|
|
131
|
+
<div className="mx-auto">
|
|
132
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
133
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
134
|
+
{/* Raison sociale */}
|
|
135
|
+
<TextInput
|
|
136
|
+
label="Raison sociale"
|
|
137
|
+
name="legal_name"
|
|
138
|
+
value={formData.legal_name || ''}
|
|
139
|
+
placeholder="Nom légal de l'organisation"
|
|
140
|
+
required
|
|
141
|
+
error={errors.legal_name}
|
|
142
|
+
onChange={handleInputChange}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
{/* Nom commercial */}
|
|
146
|
+
<TextInput
|
|
147
|
+
label="Nom commercial"
|
|
148
|
+
name="trading_name"
|
|
149
|
+
value={formData.trading_name || ''}
|
|
150
|
+
placeholder="Nom commercial (optionnel)"
|
|
151
|
+
onChange={handleInputChange}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{/* Contact */}
|
|
155
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
156
|
+
<InputField
|
|
157
|
+
label="Téléphone"
|
|
158
|
+
name="phone"
|
|
159
|
+
type="tel"
|
|
160
|
+
value={formData.phone || ''}
|
|
161
|
+
placeholder="+225 XX XX XXX XXX"
|
|
162
|
+
onChange={handleInputChange}
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
<InputField
|
|
166
|
+
label="Email"
|
|
167
|
+
name="email"
|
|
168
|
+
type="email"
|
|
169
|
+
value={formData.email || ''}
|
|
170
|
+
placeholder="contact@organisation.com"
|
|
171
|
+
error={errors.email}
|
|
172
|
+
onChange={handleInputChange}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Adresse */}
|
|
177
|
+
<div>
|
|
178
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
179
|
+
Adresse
|
|
180
|
+
</label>
|
|
181
|
+
<textarea
|
|
182
|
+
name="address"
|
|
183
|
+
value={formData.address || ''}
|
|
184
|
+
onChange={handleTextareaChange}
|
|
185
|
+
rows={3}
|
|
186
|
+
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"
|
|
187
|
+
placeholder="Adresse complète de l'organisation"
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Actions */}
|
|
192
|
+
<div className="flex justify-between pt-6 border-t border-gray-200">
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
onClick={() => navigate('/organizations')}
|
|
196
|
+
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
197
|
+
>
|
|
198
|
+
Annuler
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
<PrimaryButton onClick={handleSubmit} loading={loading}>
|
|
202
|
+
{loading ? 'Création...' : 'Créer l\'organisation'}
|
|
203
|
+
</PrimaryButton>
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
</div>
|
|
207
|
+
</form>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export default CreateOrganization;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const Dashboard: React.FC = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div style={{ padding: '2rem' }}>
|
|
6
|
+
<h1>Dashboard</h1>
|
|
7
|
+
<p>Welcome to your dashboard. Here you can view your stats and manage your account.</p>
|
|
8
|
+
{/* Add dashboard widgets/components here */}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default Dashboard;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Camera, Mail, Phone, MapPin, Calendar, Edit3, Save, X, User as UserIcon } from 'lucide-react';
|
|
3
|
+
import { useToast } from '../contexts/ToastContext';
|
|
4
|
+
import { useSession, User } from '../contexts/SessionContext';
|
|
5
|
+
|
|
6
|
+
const Profile: React.FC = () => {
|
|
7
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const { success, error: showError } = useToast();
|
|
10
|
+
|
|
11
|
+
const { loggedUser } = useSession();
|
|
12
|
+
|
|
13
|
+
const [profileData, setProfileData] = useState<User | null>(loggedUser);
|
|
14
|
+
const [editData, setEditData] = useState<Partial<User>>({});
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (loggedUser) {
|
|
18
|
+
setProfileData(loggedUser);
|
|
19
|
+
setEditData(loggedUser);
|
|
20
|
+
}
|
|
21
|
+
}, [loggedUser]);
|
|
22
|
+
|
|
23
|
+
const handleEdit = () => {
|
|
24
|
+
setEditData({ ...profileData });
|
|
25
|
+
setIsEditing(true);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleCancel = () => {
|
|
29
|
+
setEditData({ ...profileData });
|
|
30
|
+
setIsEditing(false);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleSave = async () => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
// Simuler un appel API
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
38
|
+
setProfileData({ ...editData });
|
|
39
|
+
setIsEditing(false);
|
|
40
|
+
success('Profil mis à jour avec succès !');
|
|
41
|
+
} catch (error) {
|
|
42
|
+
showError('Erreur lors de la mise à jour du profil');
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleInputChange = (field: string, value: string) => {
|
|
49
|
+
setEditData(prev => ({ ...prev, [field]: value }));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const formatDate = (dateString: string) => {
|
|
53
|
+
return new Date(dateString).toLocaleDateString('fr-FR', {
|
|
54
|
+
year: 'numeric',
|
|
55
|
+
month: 'long',
|
|
56
|
+
day: 'numeric'
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getInitials = (firstName: string, lastName: string) => {
|
|
61
|
+
return `${firstName?.[0] || ''}${lastName?.[0] || ''}`.toUpperCase();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const activities = [
|
|
65
|
+
{ id: 1, action: 'Connexion', time: 'Il y a 2 heures', type: 'login' },
|
|
66
|
+
{ id: 2, action: 'Mise à jour du profil', time: 'Hier', type: 'update' },
|
|
67
|
+
{ id: 3, action: 'Nouveau projet créé', time: 'Il y a 3 jours', type: 'create' },
|
|
68
|
+
{ id: 4, action: 'Tâche complétée', time: 'Il y a 5 jours', type: 'complete' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
if (!profileData) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-6">
|
|
74
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
75
|
+
<div className="text-center">
|
|
76
|
+
<h2 className="text-xl font-semibold text-gray-900 mb-2">Chargement du profil...</h2>
|
|
77
|
+
<p className="text-gray-600">Veuillez patienter pendant le chargement de vos informations.</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-6">
|
|
86
|
+
{/* Header */}
|
|
87
|
+
<div className="flex items-center justify-between">
|
|
88
|
+
<div>
|
|
89
|
+
<h1 className="text-3xl font-bold text-gray-900">Mon Profil</h1>
|
|
90
|
+
<p className="text-gray-600 mt-2">
|
|
91
|
+
Gérez vos informations personnelles et vos préférences
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
{!isEditing ? (
|
|
95
|
+
<button
|
|
96
|
+
onClick={handleEdit}
|
|
97
|
+
className="bg-[#8290A9] text-white px-4 py-2 rounded-lg hover:bg-[#6B7C92] transition-colors flex items-center space-x-2"
|
|
98
|
+
>
|
|
99
|
+
<Edit3 className="w-4 h-4" />
|
|
100
|
+
<span>Modifier</span>
|
|
101
|
+
</button>
|
|
102
|
+
) : (
|
|
103
|
+
<div className="flex space-x-2">
|
|
104
|
+
<button
|
|
105
|
+
onClick={handleCancel}
|
|
106
|
+
className="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 transition-colors flex items-center space-x-2"
|
|
107
|
+
>
|
|
108
|
+
<X className="w-4 h-4" />
|
|
109
|
+
<span>Annuler</span>
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
onClick={handleSave}
|
|
113
|
+
disabled={loading}
|
|
114
|
+
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2 disabled:opacity-50"
|
|
115
|
+
>
|
|
116
|
+
<Save className="w-4 h-4" />
|
|
117
|
+
<span>{loading ? 'Sauvegarde...' : 'Sauvegarder'}</span>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
124
|
+
{/* Profile Card */}
|
|
125
|
+
<div className="lg:col-span-2">
|
|
126
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
127
|
+
<div className="flex items-start space-x-6">
|
|
128
|
+
{/* Avatar */}
|
|
129
|
+
<div className="relative">
|
|
130
|
+
<div className="w-24 h-24 bg-[#8290A9] rounded-full flex items-center justify-center">
|
|
131
|
+
<span className="text-white text-2xl font-bold">
|
|
132
|
+
{getInitials(profileData?.first_name || '', profileData?.last_name || '')}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
{isEditing && (
|
|
136
|
+
<button className="absolute bottom-0 right-0 bg-[#8290A9] text-white p-2 rounded-full hover:bg-[#6B7C92] transition-colors">
|
|
137
|
+
<Camera className="w-4 h-4" />
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Basic Info */}
|
|
143
|
+
<div className="flex-1 space-y-4">
|
|
144
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
145
|
+
<div>
|
|
146
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
147
|
+
Prénom
|
|
148
|
+
</label>
|
|
149
|
+
{isEditing ? (
|
|
150
|
+
<input
|
|
151
|
+
type="text"
|
|
152
|
+
value={editData.first_name || ''}
|
|
153
|
+
onChange={(e) => handleInputChange('first_name', e.target.value)}
|
|
154
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
155
|
+
/>
|
|
156
|
+
) : (
|
|
157
|
+
<p className="text-gray-900 font-medium">{profileData?.first_name}</p>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
<div>
|
|
161
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
162
|
+
Nom
|
|
163
|
+
</label>
|
|
164
|
+
{isEditing ? (
|
|
165
|
+
<input
|
|
166
|
+
type="text"
|
|
167
|
+
value={editData.last_name || ''}
|
|
168
|
+
onChange={(e) => handleInputChange('last_name', e.target.value)}
|
|
169
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
170
|
+
/>
|
|
171
|
+
) : (
|
|
172
|
+
<p className="text-gray-900 font-medium">{profileData?.last_name}</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div>
|
|
178
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
179
|
+
Nom d'utilisateur
|
|
180
|
+
</label>
|
|
181
|
+
{isEditing ? (
|
|
182
|
+
<input
|
|
183
|
+
type="text"
|
|
184
|
+
value={editData.username || ''}
|
|
185
|
+
onChange={(e) => handleInputChange('username', e.target.value)}
|
|
186
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
187
|
+
/>
|
|
188
|
+
) : (
|
|
189
|
+
<p className="text-gray-900 font-medium">{profileData?.username}</p>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Contact Information */}
|
|
197
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mt-6">
|
|
198
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
199
|
+
Informations de contact
|
|
200
|
+
</h3>
|
|
201
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
202
|
+
<div>
|
|
203
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
204
|
+
Email
|
|
205
|
+
</label>
|
|
206
|
+
{isEditing ? (
|
|
207
|
+
<input
|
|
208
|
+
type="email"
|
|
209
|
+
value={editData.email || ''}
|
|
210
|
+
onChange={(e) => handleInputChange('email', e.target.value)}
|
|
211
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
212
|
+
/>
|
|
213
|
+
) : (
|
|
214
|
+
<div className="flex items-center space-x-2">
|
|
215
|
+
<Mail className="w-4 h-4 text-gray-400" />
|
|
216
|
+
<span className="text-gray-900">{profileData?.email}</span>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
<div>
|
|
221
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
222
|
+
Téléphone
|
|
223
|
+
</label>
|
|
224
|
+
{isEditing ? (
|
|
225
|
+
<input
|
|
226
|
+
type="tel"
|
|
227
|
+
value={editData.phonenumber || ''}
|
|
228
|
+
onChange={(e) => handleInputChange('phonenumber', e.target.value)}
|
|
229
|
+
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8290A9] focus:border-transparent"
|
|
230
|
+
/>
|
|
231
|
+
) : (
|
|
232
|
+
<div className="flex items-center space-x-2">
|
|
233
|
+
<Phone className="w-4 h-4 text-gray-400" />
|
|
234
|
+
<span className="text-gray-900">{profileData?.phonenumber}</span>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
<div>
|
|
239
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
240
|
+
Statut du compte
|
|
241
|
+
</label>
|
|
242
|
+
<div className="flex items-center space-x-2">
|
|
243
|
+
<div className={`w-2 h-2 rounded-full ${profileData?.is_active ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
|
244
|
+
<span className="text-gray-900">{profileData?.is_active ? 'Actif' : 'Inactif'}</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div>
|
|
248
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
249
|
+
Rôle
|
|
250
|
+
</label>
|
|
251
|
+
<div className="flex items-center space-x-2">
|
|
252
|
+
<UserIcon className="w-4 h-4 text-gray-400" />
|
|
253
|
+
<span className="text-gray-900">
|
|
254
|
+
{profileData?.is_superuser ? 'Super Administrateur' :
|
|
255
|
+
profileData?.is_staff ? 'Staff' : 'Utilisateur'}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Sidebar */}
|
|
264
|
+
<div className="space-y-6">
|
|
265
|
+
{/* Stats */}
|
|
266
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
267
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
268
|
+
Informations générales
|
|
269
|
+
</h3>
|
|
270
|
+
<div className="space-y-3">
|
|
271
|
+
<div className="flex items-center space-x-2">
|
|
272
|
+
<Calendar className="w-4 h-4 text-gray-400" />
|
|
273
|
+
<div>
|
|
274
|
+
<p className="text-sm text-gray-600">Date d'inscription</p>
|
|
275
|
+
<p className="font-medium">{profileData?.date_joined ? formatDate(profileData.date_joined) : 'Non disponible'}</p>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
{profileData?.last_login && (
|
|
279
|
+
<div className="flex items-center space-x-2">
|
|
280
|
+
<Calendar className="w-4 h-4 text-gray-400" />
|
|
281
|
+
<div>
|
|
282
|
+
<p className="text-sm text-gray-600">Dernière connexion</p>
|
|
283
|
+
<p className="font-medium">{formatDate(profileData.last_login)}</p>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Recent Activity */}
|
|
291
|
+
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
292
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
293
|
+
Activité récente
|
|
294
|
+
</h3>
|
|
295
|
+
<div className="space-y-3">
|
|
296
|
+
{activities.map((activity) => (
|
|
297
|
+
<div key={activity.id} className="flex items-start space-x-3">
|
|
298
|
+
<div className="w-2 h-2 bg-[#8290A9] rounded-full mt-2"></div>
|
|
299
|
+
<div>
|
|
300
|
+
<p className="text-sm font-medium text-gray-900">{activity.action}</p>
|
|
301
|
+
<p className="text-xs text-gray-500">{activity.time}</p>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export default Profile;
|