Modifs MVVM

This commit is contained in:
2025-03-12 17:49:23 +01:00
parent 2b8e7085aa
commit 456d0bb4b8
12 changed files with 287 additions and 401 deletions

View File

@ -1,5 +1,5 @@
import 'package:em2rp/providers/users_provider.dart';
import 'package:em2rp/utils/auth_guard_widget.dart';
import 'package:em2rp/view_model/user_management_view_model.dart';
import 'package:em2rp/views/calendar_page.dart';
import 'package:em2rp/views/login_page.dart';
import 'package:firebase_auth/firebase_auth.dart';
@ -10,8 +10,7 @@ import 'utils/colors.dart';
import 'views/my_account_page.dart';
import 'views/user_management_page.dart';
import 'package:provider/provider.dart';
import 'providers/user_provider.dart';
import 'providers/local_auth_provider.dart'; // Ajout de l'AuthProvider
import 'providers/local_user_provider.dart';
import 'services/user_service.dart';
void main() async {
@ -27,18 +26,13 @@ void main() async {
// Injection du service UserService
Provider<UserService>(create: (_) => UserService()),
// AuthProvider pour la gestion de l'authentification
ChangeNotifierProvider<LocalAuthProvider>(
create: (context) => LocalAuthProvider()),
// LocalUserProvider pour la gestion de l'authentification
ChangeNotifierProvider<LocalUserProvider>(
create: (context) => LocalUserProvider()),
// UserProvider déjà existant
ChangeNotifierProvider<UserProvider>(
create: (context) => UserProvider()),
// Injection des ViewModels en utilisant UserService et AuthProvider
ChangeNotifierProvider<UserManagementViewModel>(
create: (context) =>
UserManagementViewModel(context.read<UserService>()),
// // Injection des Providers en utilisant UserService
ChangeNotifierProvider<UsersProvider>(
create: (context) => UsersProvider(context.read<UserService>()),
),
],
child: const MyApp(),

View File

@ -41,4 +41,23 @@ class UserModel {
'phoneNumber': phoneNumber,
};
}
UserModel copyWith({
String? firstName,
String? lastName,
String? role,
String? profilePhotoUrl,
String? email,
String? phoneNumber,
}) {
return UserModel(
uid: uid, // L'UID ne change pas
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
role: role ?? this.role,
profilePhotoUrl: profilePhotoUrl ?? this.profilePhotoUrl,
email: email ?? this.email,
phoneNumber: phoneNumber ?? this.phoneNumber,
);
}
}

View File

@ -1,46 +0,0 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import '../models/user_model.dart';
class LocalAuthProvider with ChangeNotifier {
UserModel? _currentUser;
UserModel? get currentUser => _currentUser;
String? get role => _currentUser?.role;
void setUser(UserModel user) {
_currentUser = user;
notifyListeners();
}
void clearUser() {
_currentUser = null;
notifyListeners();
}
Future<UserCredential> signInWithEmailAndPassword(
String email, String password) async {
try {
UserCredential userCredential = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
DocumentSnapshot userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(userCredential.user!.uid)
.get();
if (userDoc.exists) {
setUser(UserModel.fromMap(
userDoc.data() as Map<String, dynamic>, userDoc.id));
} else {
throw FirebaseAuthException(
code: 'user-not-found',
message: "Aucune donnée utilisateur trouvée.");
}
return userCredential;
} on FirebaseAuthException catch (e) {
throw FirebaseAuthException(code: e.code, message: e.message);
}
}
}

View File

@ -0,0 +1,108 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../models/user_model.dart';
import '../utils/firebase_storage_manager.dart';
class LocalUserProvider with ChangeNotifier {
UserModel? _currentUser;
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
UserModel? get currentUser => _currentUser;
String? get uid => _currentUser?.uid;
String? get firstName => _currentUser?.firstName;
String? get lastName => _currentUser?.lastName;
String? get role => _currentUser?.role ?? 'USER';
String? get profilePhotoUrl => _currentUser?.profilePhotoUrl;
String? get email => _currentUser?.email;
String? get phoneNumber => _currentUser?.phoneNumber;
/// Charge les données de l'utilisateur actuel
Future<void> loadUserData() async {
if (_auth.currentUser == null) return;
DocumentSnapshot userDoc =
await _firestore.collection('users').doc(_auth.currentUser!.uid).get();
if (userDoc.exists) {
setUser(UserModel.fromMap(
userDoc.data() as Map<String, dynamic>, userDoc.id));
}
}
/// Met à jour l'utilisateur
void setUser(UserModel user) {
_currentUser = user;
notifyListeners();
}
/// Efface les données utilisateur
void clearUser() {
_currentUser = null;
notifyListeners();
}
/// Mise à jour des informations utilisateur
Future<void> updateUserData(
{String? firstName, String? lastName, String? phoneNumber}) async {
if (_currentUser == null) return;
try {
await _firestore.collection('users').doc(_currentUser!.uid).set({
'firstName': firstName ?? _currentUser!.firstName,
'lastName': lastName ?? _currentUser!.lastName,
'phone': phoneNumber ?? _currentUser!.phoneNumber,
}, SetOptions(merge: true));
_currentUser = _currentUser!.copyWith(
firstName: firstName ?? _currentUser!.firstName,
lastName: lastName ?? _currentUser!.lastName,
phoneNumber: phoneNumber ?? _currentUser!.phoneNumber,
);
notifyListeners();
} catch (e) {
debugPrint('Erreur mise à jour utilisateur : $e');
}
}
/// Changement de photo de profil
Future<void> changeProfilePicture(XFile image) async {
if (_currentUser == null) return;
try {
String? newProfilePhotoUrl = await _storageManager.sendProfilePicture(
imageFile: image,
uid: _currentUser!.uid,
);
if (newProfilePhotoUrl != null) {
_firestore
.collection('users')
.doc(_currentUser!.uid)
.update({'profilePhotoUrl': newProfilePhotoUrl});
_currentUser =
_currentUser!.copyWith(profilePhotoUrl: newProfilePhotoUrl);
notifyListeners();
}
} catch (e) {
debugPrint('Erreur mise à jour photo de profil : $e');
}
}
/// Connexion
Future<UserCredential> signInWithEmailAndPassword(
String email, String password) async {
try {
UserCredential userCredential = await _auth.signInWithEmailAndPassword(
email: email, password: password);
await loadUserData();
return userCredential;
} catch (e) {
throw FirebaseAuthException(code: 'login-failed', message: e.toString());
}
}
/// Déconnexion
Future<void> signOut() async {
await _auth.signOut();
clearUser();
}
}

View File

@ -1,58 +0,0 @@
import 'package:flutter/material.dart';
class UserProvider extends ChangeNotifier {
String? _uid;
String? _firstName;
String? _lastName;
String? _role;
String? _profilePictureUrl;
String? _email;
String? _phoneNumber;
String? get uid => _uid;
String? get firstName => _firstName;
String? get lastName => _lastName;
String? get role => _role;
String? get profilePhotoUrl => _profilePictureUrl;
String? get email => _email;
String? get phoneNumber => _phoneNumber;
void setUserData(Map<String, dynamic> userData, String uid) {
_uid = uid;
_firstName = userData['firstName'];
_lastName = userData['lastName'];
_role = userData['role'] ?? 'USER';
if (userData['profilePhotoUrl'] != "") {
_profilePictureUrl = userData['profilePhotoUrl'];
}
_email = userData['email'];
_phoneNumber = userData['phoneNumber'];
notifyListeners(); // Notify listeners that state has changed
}
void clearUserData() {
_uid = null;
_firstName = null;
_lastName = null;
_role = null;
_profilePictureUrl = null;
_email = null;
_phoneNumber = null;
notifyListeners();
}
void setUserFirstName(String text) {
_firstName = text;
notifyListeners();
}
void setUserLastName(String text) {
_lastName = text;
notifyListeners();
}
void setUserPhoneNumber(String text) {
_phoneNumber = text;
notifyListeners();
}
}

View File

@ -1,8 +1,9 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/user_service.dart';
class UserManagementViewModel extends ChangeNotifier {
class UsersProvider with ChangeNotifier {
final UserService _userService;
List<UserModel> _users = [];
bool _isLoading = false;
@ -10,28 +11,33 @@ class UserManagementViewModel extends ChangeNotifier {
List<UserModel> get users => _users;
bool get isLoading => _isLoading;
UserManagementViewModel(this._userService);
UsersProvider(this._userService);
/// Récupération de tous les utilisateurs
Future<void> fetchUsers() async {
_isLoading = true;
notifyListeners();
_users = await _userService.fetchUsers();
_isLoading = false;
notifyListeners();
try {
_users = await _userService.fetchUsers();
} finally {
_isLoading = false;
notifyListeners();
}
}
/// Mise à jour d'un utilisateur
Future<void> updateUser(UserModel user) async {
await _userService.updateUser(user);
fetchUsers();
await fetchUsers();
}
/// Suppression d'un utilisateur
Future<void> deleteUser(String uid) async {
await _userService.deleteUser(uid);
fetchUsers();
await fetchUsers();
}
/// Réinitialisation du mot de passe
Future<void> resetPassword(String email) async {
await _userService.resetPassword(email);
}

View File

@ -1,7 +1,6 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/user_provider.dart';
import 'package:em2rp/views/login_page.dart';
class AuthGuard extends StatelessWidget {
@ -17,7 +16,7 @@ class AuthGuard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localAuthProvider = Provider.of<LocalAuthProvider>(context);
final localAuthProvider = Provider.of<LocalUserProvider>(context);
// Si l'utilisateur n'est pas connecté
if (localAuthProvider.currentUser == null) {

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../providers/local_auth_provider.dart';
import '../providers/local_user_provider.dart';
import 'package:provider/provider.dart';
class LoginViewModel extends ChangeNotifier {
@ -19,7 +19,7 @@ class LoginViewModel extends ChangeNotifier {
Future<void> signIn(BuildContext context) async {
final localAuthProvider =
Provider.of<LocalAuthProvider>(context, listen: false);
Provider.of<LocalUserProvider>(context, listen: false);
isLoading = true;
errorMessage = '';
highlightPasswordField = false;

View File

@ -1,8 +1,7 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:flutter/material.dart';
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
import 'package:provider/provider.dart'; // Import Provider
import 'package:em2rp/providers/user_provider.dart'; // Import UserProvider
import 'package:em2rp/utils/colors.dart';
class CalendarPage extends StatelessWidget {
@ -10,7 +9,7 @@ class CalendarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localAuthProvider = Provider.of<LocalAuthProvider>(context);
final localAuthProvider = Provider.of<LocalUserProvider>(context);
return Scaffold(
appBar: AppBar(title: const Text('Calendrier')),

View File

@ -1,240 +1,106 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/providers/user_provider.dart';
import 'package:em2rp/utils/firebase_storage_manager.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/providers/users_provider.dart';
import 'package:em2rp/views/widgets/image/profile_picture.dart';
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
class MyAccountPage extends StatefulWidget {
class MyAccountPage extends StatelessWidget {
const MyAccountPage({super.key});
@override
_MyAccountPageState createState() => _MyAccountPageState();
}
class _MyAccountPageState extends State<MyAccountPage> {
final User? user = FirebaseAuth.instance.currentUser;
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
String? profilePhotoUrl;
bool _isHoveringProfilePic = false;
final FirebaseStorageManager _storageManager =
FirebaseStorageManager(); // Instance of FirebaseStorageManager
@override
void initState() {
super.initState();
_loadUserData();
}
Future<void> _loadUserData() async {
if (user != null) {
DocumentSnapshot userData = await FirebaseFirestore.instance
.collection('users')
.doc(user!.uid)
.get();
if (userData.exists) {
if (!mounted) return;
setState(() {
_firstNameController.text = userData['firstName'] ?? '';
_lastNameController.text = userData['lastName'] ?? '';
_phoneController.text = userData['phone'] ?? '';
});
}
}
}
Future<void> _updateUserData() async {
if (user != null) {
try {
await FirebaseFirestore.instance.collection('users').doc(user!.uid).set(
{
'firstName': _firstNameController.text,
'lastName': _lastNameController.text,
'phone': _phoneController.text,
},
SetOptions(merge: true),
);
// **MISE À JOUR DU USERPROVIDER APRÈS SUCCÈS**
final userProvider = Provider.of<UserProvider>(context, listen: false);
userProvider.setUserFirstName(_firstNameController.text);
userProvider.setUserLastName(_lastNameController.text);
userProvider.setUserPhoneNumber(_phoneController.text);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Informations personnelles mises à jour avec succès!'),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de la mise à jour des informations personnelles: ${e.toString()}',
),
backgroundColor: Colors.red,
),
);
print(
'Erreur lors de la mise à jour des informations utilisateur: ${e.toString()}');
}
}
}
Future<void> _changeProfilePicture() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
); // You can also use ImageSource.camera
if (image != null) {
// Call FirebaseStorageManager to send the profile picture
String? newProfilePhotoUrl = await _storageManager.sendProfilePicture(
imageFile: image,
uid: user!.uid,
);
if (newProfilePhotoUrl != null) {
if (!mounted) return;
setState(() {
profilePhotoUrl =
newProfilePhotoUrl; // Update the profilePhotoUrl to refresh the UI
});
// Optionally, update the UserProvider if you are using it to manage user data globally
// Provider.of<UserProvider>(context, listen: false).setUserProfilePhotoUrl(newProfilePhotoUrl);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Photo de profil mise à jour avec succès!'),
backgroundColor: Colors.green, // Optional: Style for success
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Erreur lors de la mise à jour de la photo de profil.',
),
backgroundColor: Colors.red, // Optional: Style for error
),
);
}
} else {
// User cancelled image picking
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sélection de photo annulée.')),
);
}
}
@override
Widget build(BuildContext context) {
final localAuthProvider = Provider.of<LocalAuthProvider>(
context,
); // Get UserProvider instance
return Scaffold(
appBar:
AppBar(title: const Text('Mon Compte')), // More user-friendly title
drawer: MainDrawer(
currentPage: '/my_account'), // Pass UserProvider to MainDrawer
body: SingleChildScrollView(
// Added SingleChildScrollView for better responsiveness
child: Padding(
padding: const EdgeInsets.all(
24.0), // Increased padding around the main content
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
MouseRegion(
onEnter: (_) => setState(() => _isHoveringProfilePic = true),
onExit: (_) => setState(() => _isHoveringProfilePic = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap:
_changeProfilePicture, // Call _changeProfilePicture on tap
child: Stack(
alignment: Alignment.center,
children: [
ProfilePictureWidget(
userId: user!.uid,
radius:
80), // Increased radius for larger profile picture
if (_isHoveringProfilePic)
Container(
width: 160, // Slightly larger hover overlay
height: 160,
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.edit,
color: Colors.white,
size: 36, // Slightly larger edit icon
),
appBar: AppBar(title: const Text('Mon Compte')),
drawer: MainDrawer(currentPage: '/my_account'),
body: Consumer<LocalUserProvider>(
builder: (context, userProvider, child) {
final user = userProvider.currentUser;
final usersProvider = context.read<UsersProvider>();
if (user == null) {
return const Center(child: CircularProgressIndicator());
}
final firstNameController =
TextEditingController(text: user.firstName);
final lastNameController = TextEditingController(text: user.lastName);
final phoneController = TextEditingController(text: user.phoneNumber);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
//onTap: () => userProvider.changeProfilePicture(); TODO
child: Stack(
alignment: Alignment.center,
children: [
ProfilePictureWidget(
userId: user.uid,
radius: 80,
),
],
),
),
Center(
child: Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Column(
children: [
StyledTextField(
labelText: 'Prénom',
controller: firstNameController,
),
const SizedBox(height: 16),
StyledTextField(
labelText: 'Nom',
controller: lastNameController,
),
const SizedBox(height: 16),
StyledTextField(
labelText: 'Numéro de téléphone',
controller: phoneController,
),
const SizedBox(height: 16),
StyledTextField(
labelText: 'Email',
controller:
TextEditingController(text: user.email),
enabled: false,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
userProvider.updateUserData(
firstName: firstNameController.text,
lastName: lastNameController.text,
phoneNumber: phoneController.text,
);
},
child: const Text('Enregistrer'),
),
],
),
),
],
),
),
),
Center(
child: Card(
elevation: 4.0, // Ajouter un léger relief
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12.0)), // Bords arrondis
child: Padding(
padding: const EdgeInsets.all(
24.0), // Padding intérieur de la carte
child: ConstrainedBox(
// Limiter la largeur des inputs dans la carte
constraints: BoxConstraints(
maxWidth:
500), // Ajustez la largeur maximale souhaitée
child: Column(
children: [
StyledTextField(
labelText: 'Prénom',
controller: _firstNameController),
const SizedBox(height: 16),
StyledTextField(
labelText: 'Nom',
controller: _lastNameController),
const SizedBox(height: 16),
StyledTextField(
labelText: 'Numéro de téléphone',
controller: _phoneController),
const SizedBox(height: 16),
StyledTextField(
labelText: 'Email',
controller:
TextEditingController(text: user?.email ?? ''),
enabled: false,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _updateUserData,
child: const Text('Enregistrer'),
),
],
),
),
),
),
],
),
],
),
),
),
);
},
),
);
}

View File

@ -1,15 +1,14 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../view_model/user_management_view_model.dart';
class UserManagementPage extends StatelessWidget {
const UserManagementPage({super.key});
@override
Widget build(BuildContext context) {
final userViewModel = Provider.of<UserManagementViewModel>(context);
final authProvider = Provider.of<LocalAuthProvider>(context);
// final userViewModel = Provider.of<UserManagementViewModel>(context);
final authProvider = Provider.of<LocalUserProvider>(context);
if (authProvider.role != 'ADMIN') {
return Scaffold(
@ -22,46 +21,46 @@ class UserManagementPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: const Text('Gestion des Utilisateurs')),
body: userViewModel.isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: userViewModel.users.length,
itemBuilder: (context, index) {
final user = userViewModel.users[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.profilePhotoUrl)),
title: Text('${user.firstName} ${user.lastName}'),
subtitle: Text(user.email),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
// Afficher la pop-up d'édition
},
),
IconButton(
icon: const Icon(Icons.lock_reset),
onPressed: () =>
userViewModel.resetPassword(user.email),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => userViewModel.deleteUser(user.uid),
),
],
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Ajouter un utilisateur
},
child: const Icon(Icons.add),
),
// body: userViewModel.isLoading
// ? const Center(child: CircularProgressIndicator())
// : ListView.builder(
// itemCount: userViewModel.users.length,
// itemBuilder: (context, index) {
// final user = userViewModel.users[index];
// return ListTile(
// leading: CircleAvatar(
// backgroundImage: NetworkImage(user.profilePhotoUrl)),
// title: Text('${user.firstName} ${user.lastName}'),
// subtitle: Text(user.email),
// trailing: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// IconButton(
// icon: const Icon(Icons.edit),
// onPressed: () {
// // Afficher la pop-up d'édition
// },
// ),
// IconButton(
// icon: const Icon(Icons.lock_reset),
// onPressed: () =>
// userViewModel.resetPassword(user.email),
// ),
// IconButton(
// icon: const Icon(Icons.delete, color: Colors.red),
// onPressed: () => userViewModel.deleteUser(user.uid),
// ),
// ],
// ),
// );
// },
// ),
// floatingActionButton: FloatingActionButton(
// onPressed: () {
// // Ajouter un utilisateur
// },
// child: const Icon(Icons.add),
// ),
);
}
}

View File

@ -1,4 +1,4 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/calendar_page.dart';
import 'package:em2rp/views/my_account_page.dart';
@ -14,7 +14,7 @@ class MainDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<LocalAuthProvider>(
return Consumer<LocalUserProvider>(
builder: (context, userProvider, child) {
final hasUser = userProvider.currentUser != null;