From b8e4f39e4c5b1e872ca00c317e63eb9e8ff1e95d Mon Sep 17 00:00:00 2001 From: "PC-PAUL\\paulf" Date: Tue, 13 May 2025 19:39:29 +0200 Subject: [PATCH] Ajout d'utilisateur OK Ajout bouton de deconnexion --- em2rp/firestore.rules | 90 +++++++ em2rp/lib/config/env.dart | 17 ++ em2rp/lib/main.dart | 66 +++++- em2rp/lib/models/role_model.dart | 2 - em2rp/lib/pages/auth/reset_password_page.dart | 149 ++++++++++++ em2rp/lib/providers/local_user_provider.dart | 61 ++++- em2rp/lib/providers/users_provider.dart | 139 ++++++++++- .../{widgets => utils}/permission_gate.dart | 0 em2rp/lib/view_model/login_view_model.dart | 25 +- em2rp/lib/views/calendar_page.dart | 10 +- em2rp/lib/views/user_management_page.dart | 223 ++++++++++++++---- em2rp/lib/views/widgets/nav/main_drawer.dart | 2 +- em2rp/lib/widgets/custom_app_bar.dart | 65 +++++ 13 files changed, 770 insertions(+), 79 deletions(-) create mode 100644 em2rp/firestore.rules create mode 100644 em2rp/lib/config/env.dart create mode 100644 em2rp/lib/pages/auth/reset_password_page.dart rename em2rp/lib/{widgets => utils}/permission_gate.dart (100%) create mode 100644 em2rp/lib/widgets/custom_app_bar.dart diff --git a/em2rp/firestore.rules b/em2rp/firestore.rules new file mode 100644 index 0000000..afb1777 --- /dev/null +++ b/em2rp/firestore.rules @@ -0,0 +1,90 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + // Fonction pour vérifier si l'utilisateur est authentifié + function isAuthenticated() { + return request.auth != null; + } + + function getUserRole() { + let userData = get(/databases/$(database)/documents/users/$(request.auth.uid)).data; + return userData != null ? userData.role : null; + } + + // Fonction pour vérifier si l'utilisateur est un admin + function isAdmin() { + return isAuthenticated() && getUserRole() == 'ADMIN'; + } + + function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; + } + + // Nouvelle fonction pour vérifier si un CREW est assigné à un événement du client + function isAssignedToClientEvent(clientId) { + let events = getAfter(/databases/$(database)/documents/events) + .where("clientId", "==", clientId) + .where("assignedUsers." + request.auth.uid, "==", true).limit(1); + return events.size() > 0; + } + + // Fonction pour vérifier si c'est le premier utilisateur + function isFirstUser() { + return !exists(/databases/$(database)/documents/users); + } + + // Fonction pour vérifier si c'est une mise à jour de l'UID + function isUidUpdate() { + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['uid']); + } + + // Règles pour la collection users + match /users/{userId} { + allow read: if isAuthenticated() && (isAdmin() || isOwner(userId)); + // Permettre la création si admin OU si l'utilisateur crée son propre document + allow create: if isAdmin() || (isAuthenticated() && request.auth.uid == userId); + allow update: if isAdmin() || + (isOwner(userId) && + request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['phoneNumber', 'profilePhotoUrl', 'firstName', 'lastName', 'role'])); + allow delete: if isAdmin(); + } + + // Règles pour la collection clients + match /clients/{clientId} { + // Lecture : + // - Les admins peuvent tout voir + // - Les CREW ne peuvent voir que les clients liés à leurs événements + allow read: if isAdmin() || + (getUserRole() == 'CREW' && isAssignedToClientEvent(clientId)); + + // Création, modification et suppression : Seuls les admins + allow create, update, delete: if isAdmin(); + } + + // Règles pour la collection events (prestations) + match /events/{eventId} { + allow read: if isAdmin() || + (isAuthenticated() && (resource.data.assignedUsers[request.auth.uid] == true)); + allow create, update: if isAdmin(); + allow delete: if isAdmin(); + } + + // Règles pour la collection quotes (devis) + match /quotes/{quoteId} { + allow read, write: if isAdmin(); + } + + // Règles pour la collection invoices (factures) + match /invoices/{invoiceId} { + allow read, write: if isAdmin(); + } + + // Règles pour les autres collections + match /{document=**} { + // Par défaut, refuser l'accès + allow read, write: if false; + } + } +} \ No newline at end of file diff --git a/em2rp/lib/config/env.dart b/em2rp/lib/config/env.dart new file mode 100644 index 0000000..7af019d --- /dev/null +++ b/em2rp/lib/config/env.dart @@ -0,0 +1,17 @@ +class Env { + static const bool isDevelopment = true; + + // Configuration de l'auto-login en développement + static const String devAdminEmail = 'paul.fournel@em2events.fr'; + static const String devAdminPassword = + 'votre_mot_de_passe'; // À remplacer par le vrai mot de passe + + // URLs et endpoints + static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com'; + + // Configuration Firebase + static const String firebaseProjectId = 'em2rp-951dc'; + + // Autres configurations + static const int apiTimeout = 30000; // 30 secondes +} diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index 9ba1407..3bf1647 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -12,6 +12,8 @@ import 'views/user_management_page.dart'; import 'package:provider/provider.dart'; import 'providers/local_user_provider.dart'; import 'services/user_service.dart'; +import 'pages/auth/reset_password_page.dart'; +import 'config/env.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -30,7 +32,7 @@ void main() async { ChangeNotifierProvider( create: (context) => LocalUserProvider()), - // // Injection des Providers en utilisant UserService + // Injection des Providers en utilisant UserService ChangeNotifierProvider( create: (context) => UsersProvider(context.read()), ), @@ -73,14 +75,74 @@ class MyApp extends StatelessWidget { ), ), ), + home: const AutoLoginWrapper(), routes: { '/login': (context) => const LoginPage(), '/calendar': (context) => const AuthGuard(child: CalendarPage()), '/my_account': (context) => const AuthGuard(child: MyAccountPage()), '/user_management': (context) => const AuthGuard(requiredRole: "ADMIN", child: UserManagementPage()), + '/reset_password': (context) { + final args = ModalRoute.of(context)!.settings.arguments + as Map; + return ResetPasswordPage( + email: args['email'] as String, + actionCode: args['actionCode'] as String, + ); + }, }, - initialRoute: '/login', + ); + } +} + +class AutoLoginWrapper extends StatefulWidget { + const AutoLoginWrapper({super.key}); + + @override + State createState() => _AutoLoginWrapperState(); +} + +class _AutoLoginWrapperState extends State { + @override + void initState() { + super.initState(); + _autoLogin(); + } + + Future _autoLogin() async { + try { + final localAuthProvider = + Provider.of(context, listen: false); + + // Vérifier si l'utilisateur est déjà connecté + if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) { + // Connexion automatique en mode développement + await localAuthProvider.signInWithEmailAndPassword( + Env.devAdminEmail, + Env.devAdminPassword, + ); + } + + // Charger les données utilisateur + await localAuthProvider.loadUserData(); + + if (mounted) { + Navigator.of(context).pushReplacementNamed('/calendar'); + } + } catch (e) { + print('Auto login failed: $e'); + if (mounted) { + Navigator.of(context).pushReplacementNamed('/login'); + } + } + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), ); } } diff --git a/em2rp/lib/models/role_model.dart b/em2rp/lib/models/role_model.dart index 8963f72..3fddfc5 100644 --- a/em2rp/lib/models/role_model.dart +++ b/em2rp/lib/models/role_model.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - enum Permission { // Permissions liées aux prestations viewAllEvents, // Voir toutes les prestations diff --git a/em2rp/lib/pages/auth/reset_password_page.dart b/em2rp/lib/pages/auth/reset_password_page.dart new file mode 100644 index 0000000..06da411 --- /dev/null +++ b/em2rp/lib/pages/auth/reset_password_page.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:provider/provider.dart'; +import '../../providers/users_provider.dart'; + +class ResetPasswordPage extends StatefulWidget { + final String email; + final String actionCode; + + const ResetPasswordPage({ + Key? key, + required this.email, + required this.actionCode, + }) : super(key: key); + + @override + _ResetPasswordPageState createState() => _ResetPasswordPageState(); +} + +class _ResetPasswordPageState extends State { + final _formKey = GlobalKey(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _isLoading = false; + String? _errorMessage; + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _resetPassword() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + // Vérifier le code d'action + await FirebaseAuth.instance.verifyPasswordResetCode(widget.actionCode); + + // Réinitialiser le mot de passe + await FirebaseAuth.instance.confirmPasswordReset( + code: widget.actionCode, + newPassword: _passwordController.text, + ); + + // Connecter l'utilisateur automatiquement + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: widget.email, + password: _passwordController.text, + ); + + if (mounted) { + Navigator.of(context).pushReplacementNamed('/home'); + } + } catch (e) { + setState(() { + _errorMessage = 'Une erreur est survenue. Veuillez réessayer.'; + }); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Créer votre mot de passe'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Bienvenue ! Veuillez créer votre mot de passe pour continuer.', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + TextFormField( + controller: _passwordController, + decoration: const InputDecoration( + labelText: 'Nouveau mot de passe', + border: OutlineInputBorder(), + ), + obscureText: true, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un mot de passe'; + } + if (value.length < 6) { + return 'Le mot de passe doit contenir au moins 6 caractères'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _confirmPasswordController, + decoration: const InputDecoration( + labelText: 'Confirmer le mot de passe', + border: OutlineInputBorder(), + ), + obscureText: true, + validator: (value) { + if (value != _passwordController.text) { + return 'Les mots de passe ne correspondent pas'; + } + return null; + }, + ), + if (_errorMessage != null) ...[ + const SizedBox(height: 16), + Text( + _errorMessage!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: 24), + ElevatedButton( + onPressed: _isLoading ? null : _resetPassword, + child: _isLoading + ? const CircularProgressIndicator() + : const Text('Créer le mot de passe'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/em2rp/lib/providers/local_user_provider.dart b/em2rp/lib/providers/local_user_provider.dart index ed967f5..1e61f1d 100644 --- a/em2rp/lib/providers/local_user_provider.dart +++ b/em2rp/lib/providers/local_user_provider.dart @@ -22,12 +22,61 @@ class LocalUserProvider with ChangeNotifier { /// Charge les données de l'utilisateur actuel Future 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, userDoc.id)); + if (_auth.currentUser == null) { + print('No current user in Auth'); + return; + } + + print('Loading user data for: ${_auth.currentUser!.uid}'); + try { + DocumentSnapshot userDoc = await _firestore + .collection('users') + .doc(_auth.currentUser!.uid) + .get(); + + if (userDoc.exists) { + print('User document found in Firestore'); + final userData = userDoc.data() as Map; + print('User data: $userData'); + + // Si le document n'a pas d'UID, l'ajouter + if (!userData.containsKey('uid')) { + await userDoc.reference.update({'uid': _auth.currentUser!.uid}); + userData['uid'] = _auth.currentUser!.uid; + } + + setUser(UserModel.fromMap(userData, userDoc.id)); + print('User data loaded successfully'); + } else { + print('No user document found in Firestore'); + // Créer un document utilisateur par défaut + final defaultUser = UserModel( + uid: _auth.currentUser!.uid, + email: _auth.currentUser!.email ?? '', + firstName: '', + lastName: '', + role: 'USER', + phoneNumber: '', + profilePhotoUrl: '', + ); + + await _firestore.collection('users').doc(_auth.currentUser!.uid).set({ + 'uid': _auth.currentUser!.uid, + 'email': _auth.currentUser!.email, + 'firstName': '', + 'lastName': '', + 'role': 'USER', + 'phoneNumber': '', + 'profilePhotoUrl': '', + 'createdAt': FieldValue.serverTimestamp(), + }); + + setUser(defaultUser); + print('Default user document created'); + } + } catch (e) { + print('Error loading user data: $e'); + rethrow; } } diff --git a/em2rp/lib/providers/users_provider.dart b/em2rp/lib/providers/users_provider.dart index 90ef1a7..201dfc1 100644 --- a/em2rp/lib/providers/users_provider.dart +++ b/em2rp/lib/providers/users_provider.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import '../models/user_model.dart'; import '../services/user_service.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; class UsersProvider with ChangeNotifier { final UserService _userService; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; List _users = []; bool _isLoading = false; @@ -16,28 +21,146 @@ class UsersProvider with ChangeNotifier { Future fetchUsers() async { _isLoading = true; notifyListeners(); + try { - _users = await _userService.fetchUsers(); - } finally { - _isLoading = false; - notifyListeners(); + final snapshot = await _firestore.collection('users').get(); + _users = snapshot.docs + .map((doc) => UserModel.fromMap(doc.data(), doc.id)) + .toList(); + } catch (e) { + print('Error fetching users: $e'); } + + _isLoading = false; + notifyListeners(); } /// Mise à jour d'un utilisateur Future updateUser(UserModel user) async { - await _userService.updateUser(user); - await fetchUsers(); + try { + await _firestore.collection('users').doc(user.uid).update({ + 'firstName': user.firstName, + 'lastName': user.lastName, + 'email': user.email, + 'phoneNumber': user.phoneNumber, + 'role': user.role, + 'profilePhotoUrl': user.profilePhotoUrl, + }); + + final index = _users.indexWhere((u) => u.uid == user.uid); + if (index != -1) { + _users[index] = user; + notifyListeners(); + } + } catch (e) { + print('Error updating user: $e'); + rethrow; + } } /// Suppression d'un utilisateur Future deleteUser(String uid) async { - await _userService.deleteUser(uid); - await fetchUsers(); + try { + await _firestore.collection('users').doc(uid).delete(); + _users.removeWhere((user) => user.uid == uid); + notifyListeners(); + } catch (e) { + print('Error deleting user: $e'); + rethrow; + } } /// Réinitialisation du mot de passe Future resetPassword(String email) async { await _userService.resetPassword(email); } + + Future createUserWithEmailInvite(UserModel user) async { + String? authUid; + + try { + // Vérifier l'état de l'authentification + final currentUser = _auth.currentUser; + print('Current user: ${currentUser?.email}'); + + if (currentUser == null) { + throw Exception('Aucun utilisateur connecté'); + } + + // Vérifier le rôle de l'utilisateur actuel + final currentUserDoc = + await _firestore.collection('users').doc(currentUser.uid).get(); + print('Current user role: ${currentUserDoc.data()?['role']}'); + + if (currentUserDoc.data()?['role'] != 'ADMIN') { + throw Exception( + 'Seuls les administrateurs peuvent créer des utilisateurs'); + } + + try { + // Créer l'utilisateur dans Firebase Authentication + final userCredential = await _auth.createUserWithEmailAndPassword( + email: user.email, + password: 'TemporaryPassword123!', // Mot de passe temporaire + ); + + authUid = userCredential.user!.uid; + print('User created in Auth with UID: $authUid'); + + // Créer le document dans Firestore avec l'UID de Auth comme ID + await _firestore.collection('users').doc(authUid).set({ + 'uid': authUid, + 'firstName': user.firstName, + 'lastName': user.lastName, + 'email': user.email, + 'phoneNumber': user.phoneNumber, + 'role': user.role, + 'profilePhotoUrl': user.profilePhotoUrl, + 'createdAt': FieldValue.serverTimestamp(), + }); + + print('User document created in Firestore with Auth UID'); + + // Envoyer un email de réinitialisation de mot de passe + await _auth.sendPasswordResetEmail( + email: user.email, + actionCodeSettings: ActionCodeSettings( + url: 'http://localhost:63337/finishSignUp?email=${user.email}', + handleCodeInApp: true, + androidPackageName: 'com.em2rp.app', + androidInstallApp: true, + androidMinimumVersion: '12', + ), + ); + + print('Password reset email sent'); + + // Ajouter l'utilisateur à la liste locale + final newUser = UserModel( + uid: authUid, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + phoneNumber: user.phoneNumber, + role: user.role, + profilePhotoUrl: user.profilePhotoUrl, + ); + _users.add(newUser); + notifyListeners(); + } catch (e) { + // En cas d'erreur, supprimer l'utilisateur Auth si créé + if (authUid != null) { + try { + await _auth.currentUser?.delete(); + } catch (deleteError) { + print('Warning: Could not delete Auth user: $deleteError'); + } + } + rethrow; + } + } catch (e) { + print('Error creating user: $e'); + rethrow; + } + } } diff --git a/em2rp/lib/widgets/permission_gate.dart b/em2rp/lib/utils/permission_gate.dart similarity index 100% rename from em2rp/lib/widgets/permission_gate.dart rename to em2rp/lib/utils/permission_gate.dart diff --git a/em2rp/lib/view_model/login_view_model.dart b/em2rp/lib/view_model/login_view_model.dart index 5a90870..5416a76 100644 --- a/em2rp/lib/view_model/login_view_model.dart +++ b/em2rp/lib/view_model/login_view_model.dart @@ -27,23 +27,34 @@ class LoginViewModel extends ChangeNotifier { notifyListeners(); try { - await localAuthProvider.signInWithEmailAndPassword( + final userCredential = await localAuthProvider.signInWithEmailAndPassword( emailController.text.trim(), passwordController.text); print('User signed in'); + // Attendre que les données utilisateur soient chargées + await localAuthProvider.loadUserData(); + // Vérifier si le contexte est toujours valide if (context.mounted) { - // Utiliser pushReplacementNamed pour une transition propre - Navigator.of(context, rootNavigator: true) - .pushReplacementNamed('/calendar'); + // Vérifier si l'utilisateur a bien été chargé + if (localAuthProvider.currentUser != null) { + // Utiliser pushReplacementNamed pour une transition propre + Navigator.of(context, rootNavigator: true) + .pushReplacementNamed('/calendar'); + } else { + errorMessage = 'Erreur lors du chargement des données utilisateur'; + isLoading = false; + notifyListeners(); + } } } on FirebaseAuthException catch (e) { isLoading = false; - // Gérer les erreurs... + errorMessage = + e.message ?? 'Une erreur est survenue lors de la connexion'; notifyListeners(); - } finally { - // S'assurer que isLoading est remis à false même en cas d'erreur inattendue + } catch (e) { isLoading = false; + errorMessage = 'Une erreur inattendue est survenue'; notifyListeners(); } } diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index f75474a..7e7c77b 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -1,20 +1,22 @@ import 'package:em2rp/providers/local_user_provider.dart'; import 'package:flutter/material.dart'; +import 'package:em2rp/widgets/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/main_drawer.dart'; import 'package:provider/provider.dart'; // Import Provider import 'package:em2rp/utils/colors.dart'; class CalendarPage extends StatelessWidget { - const CalendarPage({super.key}); + const CalendarPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final localAuthProvider = Provider.of(context); return Scaffold( - appBar: AppBar(title: const Text('Calendrier')), - drawer: MainDrawer( - currentPage: '/calendar'), // Pass UserProvider to MainDrawer + appBar: const CustomAppBar( + title: 'Calendrier', + ), + drawer: const MainDrawer(currentPage: '/calendar'), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/em2rp/lib/views/user_management_page.dart b/em2rp/lib/views/user_management_page.dart index dc33162..f7d678e 100644 --- a/em2rp/lib/views/user_management_page.dart +++ b/em2rp/lib/views/user_management_page.dart @@ -6,8 +6,9 @@ import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/views/widgets/user_management/user_card.dart'; import 'package:em2rp/views/widgets/user_management/edit_user_dialog.dart'; import 'package:em2rp/utils/colors.dart'; -import 'package:em2rp/widgets/permission_gate.dart'; +import 'package:em2rp/utils/permission_gate.dart'; import 'package:em2rp/models/role_model.dart'; +import 'package:em2rp/widgets/custom_app_bar.dart'; class UserManagementPage extends StatefulWidget { const UserManagementPage({Key? key}) : super(key: key); @@ -29,9 +30,8 @@ class _UserManagementPageState extends State { return PermissionGate( requiredPermissions: [Permission.viewUsers], fallback: Scaffold( - appBar: AppBar( - title: const Text('Accès refusé'), - backgroundColor: AppColors.rouge, + appBar: const CustomAppBar( + title: 'Accès refusé', ), body: const Center( child: Text( @@ -42,9 +42,8 @@ class _UserManagementPageState extends State { ), ), child: Scaffold( - appBar: AppBar( - title: const Text('Gestion des utilisateurs'), - backgroundColor: AppColors.rouge, + appBar: const CustomAppBar( + title: 'Gestion des utilisateurs', ), drawer: const MainDrawer(currentPage: '/account_management'), body: Consumer( @@ -108,53 +107,179 @@ class _UserManagementPageState extends State { final lastNameController = TextEditingController(); final emailController = TextEditingController(); final phoneController = TextEditingController(); - final roleController = TextEditingController(); + String selectedRole = Roles.values.first.name; + + InputDecoration _buildInputDecoration(String label, IconData icon) { + return InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: AppColors.rouge), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.rouge, width: 2), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ); + } showDialog( context: context, - builder: (context) => AlertDialog( - title: const Text('Créer un utilisateur'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: firstNameController, - decoration: const InputDecoration(labelText: 'Prénom')), - TextField( - controller: lastNameController, - decoration: const InputDecoration(labelText: 'Nom')), - TextField( - controller: emailController, - decoration: const InputDecoration(labelText: 'Email')), - TextField( - controller: phoneController, - decoration: const InputDecoration(labelText: 'Téléphone')), - TextField( - controller: roleController, - decoration: const InputDecoration(labelText: 'Rôle')), - ], + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Annuler'), + child: Container( + width: 400, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const Icon(Icons.person_add, color: AppColors.rouge), + const SizedBox(width: 12), + Text( + 'Nouvel utilisateur', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppColors.noir, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 24), + SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: firstNameController, + decoration: + _buildInputDecoration('Prénom', Icons.person_outline), + ), + const SizedBox(height: 16), + TextField( + controller: lastNameController, + decoration: _buildInputDecoration('Nom', Icons.person), + ), + const SizedBox(height: 16), + TextField( + controller: emailController, + decoration: + _buildInputDecoration('Email', Icons.email_outlined), + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + TextField( + controller: phoneController, + decoration: _buildInputDecoration( + 'Téléphone', Icons.phone_outlined), + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + value: selectedRole, + decoration: _buildInputDecoration( + 'Rôle', Icons.admin_panel_settings_outlined), + items: Roles.values.map((Role role) { + return DropdownMenuItem( + value: role.name, + child: Text(role.name), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + selectedRole = newValue; + } + }, + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + ), + child: const Text( + 'Annuler', + style: TextStyle(color: AppColors.gris), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () async { + if (emailController.text.isEmpty || + firstNameController.text.isEmpty || + lastNameController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Veuillez remplir tous les champs obligatoires'), + backgroundColor: Colors.red, + ), + ); + return; + } + + try { + final newUser = UserModel( + uid: '', // Sera généré par Firebase + firstName: firstNameController.text, + lastName: lastNameController.text, + email: emailController.text, + phoneNumber: phoneController.text, + role: selectedRole, + profilePhotoUrl: '', + ); + + await Provider.of(context, listen: false) + .createUserWithEmailInvite(newUser); + + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invitation envoyée avec succès'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Erreur lors de la création: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.rouge, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Inviter', + style: TextStyle(color: AppColors.blanc), + ), + ), + ], + ), + ], ), - TextButton( - onPressed: () { - final newUser = UserModel( - uid: '', - firstName: firstNameController.text, - lastName: lastNameController.text, - email: emailController.text, - phoneNumber: phoneController.text, - role: roleController.text, - profilePhotoUrl: '', - ); - Navigator.pop(context); - }, - child: const Text('Créer'), - ), - ], + ), ), ); } diff --git a/em2rp/lib/views/widgets/nav/main_drawer.dart b/em2rp/lib/views/widgets/nav/main_drawer.dart index c4d1e3b..64319f0 100644 --- a/em2rp/lib/views/widgets/nav/main_drawer.dart +++ b/em2rp/lib/views/widgets/nav/main_drawer.dart @@ -6,7 +6,7 @@ import 'package:em2rp/views/user_management_page.dart'; import 'package:flutter/material.dart'; import 'package:em2rp/views/widgets/image/profile_picture.dart'; import 'package:provider/provider.dart'; -import 'package:em2rp/widgets/permission_gate.dart'; +import 'package:em2rp/utils/permission_gate.dart'; import 'package:em2rp/models/role_model.dart'; class MainDrawer extends StatelessWidget { diff --git a/em2rp/lib/widgets/custom_app_bar.dart b/em2rp/lib/widgets/custom_app_bar.dart new file mode 100644 index 0000000..b045b64 --- /dev/null +++ b/em2rp/lib/widgets/custom_app_bar.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:em2rp/providers/local_user_provider.dart'; +import 'package:em2rp/utils/colors.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List? actions; + final bool showLogoutButton; + + const CustomAppBar({ + Key? key, + required this.title, + this.actions, + this.showLogoutButton = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text(title), + backgroundColor: AppColors.rouge, + actions: [ + if (showLogoutButton) + IconButton( + icon: const Icon(Icons.logout, color: AppColors.blanc), + onPressed: () async { + // Afficher une boîte de dialogue de confirmation + final shouldLogout = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Déconnexion'), + content: + const Text('Voulez-vous vraiment vous déconnecter ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Déconnexion'), + ), + ], + ), + ); + + if (shouldLogout == true) { + // Déconnexion + await Provider.of(context, listen: false) + .signOut(); + if (context.mounted) { + Navigator.of(context).pushReplacementNamed('/login'); + } + } + }, + ), + if (actions != null) ...actions!, + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +}