Compare commits

...

10 Commits

32 changed files with 3095 additions and 491 deletions

90
em2rp/firestore.rules Normal file
View File

@ -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;
}
}
}

17
em2rp/lib/config/env.dart Normal file
View File

@ -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 =
"Azerty\$1!"; // À 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
}

View File

@ -1,5 +1,6 @@
import 'package:em2rp/providers/users_provider.dart';
import 'package:em2rp/providers/event_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,9 +11,10 @@ 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';
import 'pages/auth/reset_password_page.dart';
import 'config/env.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -27,18 +29,18 @@ 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 Providers en utilisant UserService
ChangeNotifierProvider<UsersProvider>(
create: (context) => UsersProvider(context.read<UserService>()),
),
// Injection des ViewModels en utilisant UserService et AuthProvider
ChangeNotifierProvider<UserManagementViewModel>(
create: (context) =>
UserManagementViewModel(context.read<UserService>()),
// EventProvider pour la gestion des événements
ChangeNotifierProvider<EventProvider>(
create: (context) => EventProvider(),
),
],
child: const MyApp(),
@ -62,7 +64,7 @@ class MyApp extends StatelessWidget {
textTheme: const TextTheme(
bodyMedium: TextStyle(color: AppColors.noir),
),
inputDecorationTheme: InputDecorationTheme(
inputDecorationTheme: const InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.noir),
),
@ -79,14 +81,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<String, dynamic>;
return ResetPasswordPage(
email: args['email'] as String,
actionCode: args['actionCode'] as String,
);
},
},
initialRoute: '/login',
);
}
}
class AutoLoginWrapper extends StatefulWidget {
const AutoLoginWrapper({super.key});
@override
State<AutoLoginWrapper> createState() => _AutoLoginWrapperState();
}
class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
@override
void initState() {
super.initState();
_autoLogin();
}
Future<void> _autoLogin() async {
try {
final localAuthProvider =
Provider.of<LocalUserProvider>(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(),
),
);
}
}

View File

@ -0,0 +1,82 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:latlong2/latlong.dart';
class EventModel {
final String id;
final String name;
final String description;
final DateTime startDateTime;
final DateTime endDateTime;
final double price;
final int installationTime;
final int disassemblyTime;
final String eventTypeId;
final String customerId;
final LatLng address;
final List<String> workforce;
EventModel({
required this.id,
required this.name,
required this.description,
required this.startDateTime,
required this.endDateTime,
required this.price,
required this.installationTime,
required this.disassemblyTime,
required this.eventTypeId,
required this.customerId,
required this.address,
required this.workforce,
});
factory EventModel.fromMap(Map<String, dynamic> map, String id) {
final GeoPoint? geoPoint = map['Address'] as GeoPoint?;
final List<dynamic> workforceRefs = map['workforce'] ?? [];
final Timestamp? startTimestamp = map['StartDateTime'] as Timestamp?;
final Timestamp? endTimestamp = map['EndDateTime'] as Timestamp?;
return EventModel(
id: id,
name: map['Name'] ?? '',
description: map['Description'] ?? '',
startDateTime: startTimestamp?.toDate() ?? DateTime.now(),
endDateTime: endTimestamp?.toDate() ??
DateTime.now().add(const Duration(hours: 1)),
price: (map['Price'] ?? 0.0).toDouble(),
installationTime: map['InstallationTime'] ?? 0,
disassemblyTime: map['DisassemblyTime'] ?? 0,
eventTypeId: map['EventType'] is DocumentReference
? (map['EventType'] as DocumentReference).id
: '',
customerId: map['customer'] is DocumentReference
? (map['customer'] as DocumentReference).id
: '',
address: geoPoint != null
? LatLng(geoPoint.latitude, geoPoint.longitude)
: const LatLng(0, 0),
workforce: workforceRefs.map((ref) {
if (ref is DocumentReference) {
return ref.id;
}
return ref.toString();
}).toList(),
);
}
Map<String, dynamic> toMap() {
return {
'Name': name,
'Description': description,
'StartDateTime': Timestamp.fromDate(startDateTime),
'EndDateTime': Timestamp.fromDate(endDateTime),
'Price': price,
'InstallationTime': installationTime,
'DisassemblyTime': disassemblyTime,
'EventType': eventTypeId,
'customer': customerId,
'Address': GeoPoint(address.latitude, address.longitude),
'workforce': workforce,
};
}
}

View File

@ -0,0 +1,100 @@
enum Permission {
// Permissions liées aux prestations
viewAllEvents, // Voir toutes les prestations
viewAssignedEvents, // Voir uniquement les prestations assignées
editEvents, // Modifier les prestations
deleteEvents, // Supprimer les prestations
assignCrew, // Assigner des membres d'équipe aux prestations
// Permissions liées aux finances
viewPrices, // Voir les prix
editPrices, // Modifier les prix
viewQuotes, // Voir les devis
createQuotes, // Créer des devis
editQuotes, // Modifier les devis
viewInvoices, // Voir les factures
createInvoices, // Créer des factures
editInvoices, // Modifier les factures
// Permissions liées aux utilisateurs
viewUsers, // Voir les utilisateurs
editUsers, // Modifier les utilisateurs
deleteUsers, // Supprimer les utilisateurs
// Permissions liées aux clients
viewClients, // Voir les clients
editClients, // Modifier les clients
deleteClients, // Supprimer les clients
}
class Role {
final String name;
final Set<Permission> permissions;
const Role({
required this.name,
required this.permissions,
});
bool hasPermission(Permission permission) => permissions.contains(permission);
bool hasAllPermissions(List<Permission> requiredPermissions) {
return requiredPermissions
.every((permission) => permissions.contains(permission));
}
bool hasAnyPermission(List<Permission> requiredPermissions) {
return requiredPermissions
.any((permission) => permissions.contains(permission));
}
}
class Roles {
static const admin = Role(
name: 'ADMIN',
permissions: {
// Toutes les permissions pour l'administrateur
Permission.viewAllEvents,
Permission.viewAssignedEvents,
Permission.editEvents,
Permission.deleteEvents,
Permission.assignCrew,
Permission.viewPrices,
Permission.editPrices,
Permission.viewQuotes,
Permission.createQuotes,
Permission.editQuotes,
Permission.viewInvoices,
Permission.createInvoices,
Permission.editInvoices,
Permission.viewUsers,
Permission.editUsers,
Permission.deleteUsers,
Permission.viewClients,
Permission.editClients,
Permission.deleteClients,
},
);
static const crew = Role(
name: 'CREW',
permissions: {
// Permissions limitées pour l'équipe
Permission.viewAssignedEvents,
Permission.viewClients,
},
);
static Role fromString(String roleName) {
switch (roleName.toUpperCase()) {
case 'ADMIN':
return admin;
case 'CREW':
return crew;
default:
return crew; // Par défaut, on donne les permissions minimales
}
}
static List<Role> values = [admin, crew];
}

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

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class ResetPasswordPage extends StatefulWidget {
final String email;
final String actionCode;
const ResetPasswordPage({
super.key,
required this.email,
required this.actionCode,
});
@override
ResetPasswordPageState createState() => ResetPasswordPageState();
}
class ResetPasswordPageState extends State<ResetPasswordPage> {
final _formKey = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
String? _errorMessage;
@override
void dispose() {
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _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'),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,105 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../models/event_model.dart';
class EventProvider with ChangeNotifier {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
List<EventModel> _events = [];
bool _isLoading = false;
List<EventModel> get events => _events;
bool get isLoading => _isLoading;
// Récupérer les événements pour un utilisateur spécifique
Future<void> loadUserEvents(String userId) async {
_isLoading = true;
notifyListeners();
try {
print('Loading events for user: $userId');
// Récupérer uniquement les événements où l'utilisateur est dans la workforce
final eventsSnapshot = await _firestore
.collection('events')
.where('workforce', arrayContains: userId)
.get();
print('Found ${eventsSnapshot.docs.length} events for user');
_events = eventsSnapshot.docs.map((doc) {
print('Event data: ${doc.data()}');
return EventModel.fromMap(doc.data(), doc.id);
}).toList();
print('Parsed ${_events.length} events');
_isLoading = false;
notifyListeners();
} catch (e) {
print('Error loading events: $e');
_isLoading = false;
notifyListeners();
rethrow;
}
}
// Récupérer un événement spécifique
Future<EventModel?> getEvent(String eventId) async {
try {
final doc = await _firestore.collection('events').doc(eventId).get();
if (doc.exists) {
return EventModel.fromMap(doc.data()!, doc.id);
}
return null;
} catch (e) {
print('Error getting event: $e');
rethrow;
}
}
// Ajouter un nouvel événement
Future<void> addEvent(EventModel event) async {
try {
final docRef = await _firestore.collection('events').add(event.toMap());
final newEvent = EventModel.fromMap(event.toMap(), docRef.id);
_events.add(newEvent);
notifyListeners();
} catch (e) {
print('Error adding event: $e');
rethrow;
}
}
// Mettre à jour un événement
Future<void> updateEvent(EventModel event) async {
try {
await _firestore.collection('events').doc(event.id).update(event.toMap());
final index = _events.indexWhere((e) => e.id == event.id);
if (index != -1) {
_events[index] = event;
notifyListeners();
}
} catch (e) {
print('Error updating event: $e');
rethrow;
}
}
// Supprimer un événement
Future<void> deleteEvent(String eventId) async {
try {
await _firestore.collection('events').doc(eventId).delete();
_events.removeWhere((event) => event.id == eventId);
notifyListeners();
} catch (e) {
print('Error deleting event: $e');
rethrow;
}
}
// Vider la liste des événements
void clearEvents() {
_events = [];
notifyListeners();
}
}

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,157 @@
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) {
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<String, dynamic>;
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;
}
}
/// 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

@ -0,0 +1,166 @@
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<UserModel> _users = [];
bool _isLoading = false;
List<UserModel> get users => _users;
bool get isLoading => _isLoading;
UsersProvider(this._userService);
/// Récupération de tous les utilisateurs
Future<void> fetchUsers() async {
_isLoading = true;
notifyListeners();
try {
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<void> updateUser(UserModel user) async {
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<void> deleteUser(String uid) async {
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<void> resetPassword(String email) async {
await _userService.resetPassword(email);
}
Future<void> 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;
}
}
}

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

@ -0,0 +1,115 @@
import 'package:em2rp/models/event_model.dart';
class CalendarUtils {
static String getDayName(int weekday) {
switch (weekday) {
case DateTime.monday:
return 'Lundi';
case DateTime.tuesday:
return 'Mardi';
case DateTime.wednesday:
return 'Mercredi';
case DateTime.thursday:
return 'Jeudi';
case DateTime.friday:
return 'Vendredi';
case DateTime.saturday:
return 'Samedi';
case DateTime.sunday:
return 'Dimanche';
default:
return '';
}
}
static String getShortDayName(int weekday) {
switch (weekday) {
case DateTime.monday:
return 'Lun';
case DateTime.tuesday:
return 'Mar';
case DateTime.wednesday:
return 'Mer';
case DateTime.thursday:
return 'Jeu';
case DateTime.friday:
return 'Ven';
case DateTime.saturday:
return 'Sam';
case DateTime.sunday:
return 'Dim';
default:
return '';
}
}
static String getMonthName(int month) {
switch (month) {
case 1:
return 'Janvier';
case 2:
return 'Février';
case 3:
return 'Mars';
case 4:
return 'Avril';
case 5:
return 'Mai';
case 6:
return 'Juin';
case 7:
return 'Juillet';
case 8:
return 'Août';
case 9:
return 'Septembre';
case 10:
return 'Octobre';
case 11:
return 'Novembre';
case 12:
return 'Décembre';
default:
return '';
}
}
static String getMonthYearString(DateTime weekStart, DateTime weekEnd) {
if (weekStart.month == weekEnd.month) {
return '${getMonthName(weekStart.month)} ${weekStart.year}';
} else {
return '${getMonthName(weekStart.month)} - ${getMonthName(weekEnd.month)} ${weekEnd.year}';
}
}
static bool isMultiDayEvent(EventModel event) {
return event.startDateTime.day != event.endDateTime.day ||
event.startDateTime.month != event.endDateTime.month ||
event.startDateTime.year != event.endDateTime.year;
}
static int calculateTotalDays(EventModel event) {
final startDate = DateTime(event.startDateTime.year,
event.startDateTime.month, event.startDateTime.day);
final endDate = DateTime(
event.endDateTime.year, event.endDateTime.month, event.endDateTime.day);
return endDate.difference(startDate).inDays + 1;
}
static int calculateDayNumber(DateTime startDate, DateTime currentDay) {
final start = DateTime(startDate.year, startDate.month, startDate.day);
final current = DateTime(currentDay.year, currentDay.month, currentDay.day);
return current.difference(start).inDays + 1;
}
static List<EventModel> getEventsForDay(
DateTime day, List<EventModel> events) {
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
return events.where((event) {
return !(event.endDateTime.isBefore(dayStart) ||
event.startDateTime.isAfter(dayEnd));
}).toList();
}
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/models/role_model.dart';
import 'package:em2rp/providers/local_user_provider.dart';
class PermissionGate extends StatelessWidget {
final Widget child;
final List<Permission> requiredPermissions;
final bool requireAll;
final Widget? fallback;
const PermissionGate({
super.key,
required this.child,
required this.requiredPermissions,
this.requireAll = true,
this.fallback,
});
@override
Widget build(BuildContext context) {
return Consumer<LocalUserProvider>(
builder: (context, localUserProvider, _) {
final currentUser = localUserProvider.currentUser;
if (currentUser == null) {
return fallback ?? const SizedBox.shrink();
}
final userRole = Roles.fromString(currentUser.role);
final hasPermission = requireAll
? userRole.hasAllPermissions(requiredPermissions)
: userRole.hasAnyPermission(requiredPermissions);
if (hasPermission) {
return child;
}
return fallback ?? const SizedBox.shrink();
},
);
}
}

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;
@ -27,23 +27,32 @@ class LoginViewModel extends ChangeNotifier {
notifyListeners();
try {
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();
}
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/user_service.dart';
class UserManagementViewModel extends ChangeNotifier {
final UserService _userService;
List<UserModel> _users = [];
bool _isLoading = false;
List<UserModel> get users => _users;
bool get isLoading => _isLoading;
UserManagementViewModel(this._userService);
Future<void> fetchUsers() async {
_isLoading = true;
notifyListeners();
_users = await _userService.fetchUsers();
_isLoading = false;
notifyListeners();
}
Future<void> updateUser(UserModel user) async {
await _userService.updateUser(user);
fetchUsers();
}
Future<void> deleteUser(String uid) async {
await _userService.deleteUser(uid);
fetchUsers();
}
Future<void> resetPassword(String email) async {
await _userService.resetPassword(email);
}
}

View File

@ -1,36 +1,182 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/providers/event_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/providers/user_provider.dart'; // Import UserProvider
import 'package:em2rp/utils/colors.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/widgets/event_details.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
class CalendarPage extends StatelessWidget {
class CalendarPage extends StatefulWidget {
const CalendarPage({super.key});
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
EventModel? _selectedEvent;
@override
void initState() {
super.initState();
initializeDateFormatting('fr_FR', null);
Future.microtask(() => _loadEvents());
}
Future<void> _loadEvents() async {
final localAuthProvider =
Provider.of<LocalUserProvider>(context, listen: false);
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final userId = localAuthProvider.uid;
if (userId != null) {
await eventProvider.loadUserEvents(userId);
}
}
void _changeWeek(int delta) {
setState(() {
_focusedDay = _focusedDay.add(Duration(days: 7 * delta));
});
}
@override
Widget build(BuildContext context) {
final localAuthProvider = Provider.of<LocalAuthProvider>(context);
final eventProvider = Provider.of<EventProvider>(context);
final isMobile = MediaQuery.of(context).size.width < 600;
if (eventProvider.isLoading) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Calendrier')),
drawer: MainDrawer(
currentPage: '/calendar'), // Pass UserProvider to MainDrawer
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Page Calendrier', style: TextStyle(fontSize: 24)),
const SizedBox(height: 20),
if (localAuthProvider.role == 'ADMIN') // Get role from UserProvider
const Text('Vue Admin du Calendrier',
style: TextStyle(fontSize: 18, color: AppColors.rouge))
else
const Text('Vue Utilisateur du Calendrier',
style: TextStyle(fontSize: 18, color: Colors.blueGrey)),
],
),
appBar: const CustomAppBar(
title: 'Calendrier',
),
drawer: const MainDrawer(currentPage: '/calendar'),
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
);
}
Widget _buildDesktopLayout() {
final eventProvider = Provider.of<EventProvider>(context);
return Row(
children: [
// Calendrier (65% de la largeur)
Expanded(
flex: 65,
child: _buildCalendar(),
),
// Détails de l'événement (35% de la largeur)
Expanded(
flex: 35,
child: _selectedEvent != null
? EventDetails(
event: _selectedEvent!,
selectedDate: _selectedDay,
events: eventProvider.events,
onSelectEvent: (event, date) {
setState(() {
_selectedEvent = event;
_selectedDay = date;
});
},
)
: const Center(
child:
Text('Sélectionnez un événement pour voir les détails'),
),
),
],
);
}
Widget _buildMobileLayout() {
final eventProvider = Provider.of<EventProvider>(context);
return Column(
children: [
// Calendrier
Expanded(
child: _buildCalendar(),
),
// Détails de l'événement
if (_selectedEvent != null)
Expanded(
child: EventDetails(
event: _selectedEvent!,
selectedDate: _selectedDay,
events: eventProvider.events,
onSelectEvent: (event, date) {
setState(() {
_selectedEvent = event;
_selectedDay = date;
});
},
),
),
],
);
}
Widget _buildCalendar() {
final eventProvider = Provider.of<EventProvider>(context);
if (_calendarFormat == CalendarFormat.week) {
return WeekView(
focusedDay: _focusedDay,
events: eventProvider.events,
onWeekChange: _changeWeek,
onEventSelected: (event) {
setState(() {
_selectedEvent = event;
});
},
onSwitchToMonth: () {
setState(() {
_calendarFormat = CalendarFormat.month;
});
},
);
} else {
return MonthView(
focusedDay: _focusedDay,
selectedDay: _selectedDay,
calendarFormat: _calendarFormat,
events: eventProvider.events,
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() {
_calendarFormat = format;
});
},
onPageChanged: (focusedDay) {
setState(() {
_focusedDay = focusedDay;
});
},
onEventSelected: (event) {
setState(() {
_selectedEvent = event;
});
},
);
}
}
}

View File

@ -1,240 +1,96 @@
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/views/widgets/image/profile_picture.dart';
import 'package:em2rp/providers/local_user_provider.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';
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
import 'package:em2rp/widgets/custom_app_bar.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: const CustomAppBar(
title: 'Mon compte',
),
drawer: const MainDrawer(currentPage: '/my_account'),
body: Consumer<LocalUserProvider>(
builder: (context, userProvider, child) {
final user = userProvider.currentUser;
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: [
const ProfilePictureSelector(),
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,66 +1,293 @@
import 'package:em2rp/providers/local_auth_provider.dart';
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../view_model/user_management_view_model.dart';
import 'package:em2rp/providers/users_provider.dart';
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/utils/permission_gate.dart';
import 'package:em2rp/models/role_model.dart';
import 'package:em2rp/widgets/custom_app_bar.dart';
class UserManagementPage extends StatelessWidget {
class UserManagementPage extends StatefulWidget {
const UserManagementPage({super.key});
@override
Widget build(BuildContext context) {
final userViewModel = Provider.of<UserManagementViewModel>(context);
final authProvider = Provider.of<LocalAuthProvider>(context);
State<UserManagementPage> createState() => _UserManagementPageState();
}
if (authProvider.role != 'ADMIN') {
return Scaffold(
appBar: AppBar(title: const Text('Gestion des Utilisateurs')),
body: const Center(
child: Text('Accès non autorisé',
style: TextStyle(color: Colors.red))),
class _UserManagementPageState extends State<UserManagementPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Provider.of<UsersProvider>(context, listen: false).fetchUsers();
}
});
}
@override
Widget build(BuildContext context) {
return PermissionGate(
requiredPermissions: const [Permission.viewUsers],
fallback: const Scaffold(
appBar: CustomAppBar(
title: 'Accès refusé',
),
body: Center(
child: Text(
'Vous n\'avez pas les permissions nécessaires pour accéder à cette page.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
),
),
child: Scaffold(
appBar: const CustomAppBar(
title: 'Gestion des utilisateurs',
),
drawer: const MainDrawer(currentPage: '/account_management'),
body: Consumer<UsersProvider>(
builder: (context, usersProvider, child) {
if (usersProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final users = usersProvider.users;
if (users.isEmpty) {
return const Center(child: Text("Aucun utilisateur trouvé"));
}
final width = MediaQuery.of(context).size.width;
int crossAxisCount;
if (width > 1200) {
crossAxisCount = 4;
} else if (width > 800) {
crossAxisCount = 3;
} else if (width > 600) {
crossAxisCount = 2;
} else {
crossAxisCount = 1;
}
return Padding(
padding: const EdgeInsets.all(16),
child: GridView.builder(
itemCount: users.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
mainAxisExtent: width < 600 ? 80 : 180,
),
itemBuilder: (context, i) {
final user = users[i];
return UserCard(
user: user,
onEdit: () => showDialog(
context: context,
builder: (_) => EditUserDialog(user: user)),
onDelete: () => usersProvider.deleteUser(user.uid),
);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: AppColors.rouge,
child: const Icon(Icons.add, color: AppColors.blanc),
onPressed: () => _showCreateUserDialog(context),
),
),
);
}
void _showCreateUserDialog(BuildContext context) {
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = 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),
);
}
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),
),
],
showDialog(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
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,
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Ajouter un utilisateur
},
child: const Icon(Icons.add),
],
),
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<String>(
value: selectedRole,
decoration: buildInputDecoration(
'Rôle', Icons.admin_panel_settings_outlined),
items: Roles.values.map((Role role) {
return DropdownMenuItem<String>(
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: '',
);
final scaffoldMessenger = ScaffoldMessenger.of(context);
await Provider.of<UsersProvider>(context, listen: false)
.createUserWithEmailInvite(newUser);
if (context.mounted) {
Navigator.pop(context);
scaffoldMessenger.showSnackBar(
const SnackBar(
content: Text('Invitation envoyée avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (context.mounted) {
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),
),
),
],
),
],
),
),
),
);
}

View File

@ -6,7 +6,7 @@ class WelcomeTextWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
return const Text(
'Bienvenue !',
textAlign: TextAlign.center,
style: TextStyle(

View File

@ -0,0 +1,257 @@
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/calendar_utils.dart';
class MonthView extends StatelessWidget {
final DateTime focusedDay;
final DateTime? selectedDay;
final CalendarFormat calendarFormat;
final Function(DateTime, DateTime) onDaySelected;
final Function(CalendarFormat) onFormatChanged;
final Function(DateTime) onPageChanged;
final List<EventModel> events;
final Function(EventModel) onEventSelected;
const MonthView({
super.key,
required this.focusedDay,
required this.selectedDay,
required this.calendarFormat,
required this.onDaySelected,
required this.onFormatChanged,
required this.onPageChanged,
required this.events,
required this.onEventSelected,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final rowHeight = (constraints.maxHeight - 100) / 6;
return Container(
height: constraints.maxHeight,
padding: const EdgeInsets.all(8),
child: TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: focusedDay,
calendarFormat: calendarFormat,
startingDayOfWeek: StartingDayOfWeek.monday,
locale: 'fr_FR',
availableCalendarFormats: const {
CalendarFormat.month: 'Mois',
CalendarFormat.week: 'Semaine',
},
selectedDayPredicate: (day) => isSameDay(selectedDay, day),
onDaySelected: onDaySelected,
onFormatChanged: onFormatChanged,
onPageChanged: onPageChanged,
calendarStyle: _buildCalendarStyle(),
rowHeight: rowHeight,
headerStyle: _buildHeaderStyle(),
calendarBuilders: _buildCalendarBuilders(),
),
);
},
);
}
CalendarStyle _buildCalendarStyle() {
return CalendarStyle(
defaultDecoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
selectedDecoration: BoxDecoration(
color: AppColors.rouge,
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
todayDecoration: BoxDecoration(
color: AppColors.rouge.withAlpha(26),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
outsideDecoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
outsideDaysVisible: false,
cellMargin: EdgeInsets.zero,
cellPadding: EdgeInsets.zero,
);
}
HeaderStyle _buildHeaderStyle() {
return HeaderStyle(
formatButtonVisible: true,
titleCentered: true,
formatButtonShowsNext: false,
formatButtonDecoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: BorderRadius.circular(16),
),
formatButtonTextStyle: const TextStyle(color: Colors.white),
leftChevronIcon: const Icon(Icons.chevron_left, color: AppColors.rouge),
rightChevronIcon: const Icon(Icons.chevron_right, color: AppColors.rouge),
headerPadding: const EdgeInsets.symmetric(vertical: 8),
titleTextStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
);
}
CalendarBuilders _buildCalendarBuilders() {
return CalendarBuilders(
dowBuilder: (context, day) {
return Center(
child: Text(
CalendarUtils.getShortDayName(day.weekday),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
defaultBuilder: (context, day, focusedDay) {
return _buildDayCell(day, false);
},
selectedBuilder: (context, day, focusedDay) {
return _buildDayCell(day, true);
},
todayBuilder: (context, day, focusedDay) {
return _buildDayCell(day, false, isToday: true);
},
);
}
Widget _buildDayCell(DateTime day, bool isSelected, {bool isToday = false}) {
final dayEvents = CalendarUtils.getEventsForDay(day, events);
final textColor =
isSelected ? Colors.white : (isToday ? AppColors.rouge : null);
final badgeColor = isSelected ? Colors.white : AppColors.rouge;
final badgeTextColor = isSelected ? AppColors.rouge : Colors.white;
BoxDecoration decoration;
if (isSelected) {
decoration = BoxDecoration(
color: AppColors.rouge,
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
);
} else if (isToday) {
decoration = BoxDecoration(
color: AppColors.rouge.withAlpha(26),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
);
} else {
decoration = BoxDecoration(
color: null,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
);
}
return Container(
margin: const EdgeInsets.all(4),
decoration: decoration,
child: Stack(
children: [
Positioned(
top: 4,
left: 4,
child: Text(
day.day.toString(),
style: TextStyle(color: textColor),
),
),
if (dayEvents.isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: badgeColor,
borderRadius: BorderRadius.circular(10),
),
child: Text(
dayEvents.length.toString(),
style: TextStyle(
color: badgeTextColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
if (dayEvents.isNotEmpty)
Positioned(
bottom: 2,
left: 2,
right: 2,
top: 28,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: dayEvents
.map((event) => _buildEventItem(event, isSelected, day))
.toList(),
),
),
),
],
),
);
}
Widget _buildEventItem(
EventModel event, bool isSelected, DateTime currentDay) {
return GestureDetector(
onTap: () {
onDaySelected(currentDay, currentDay);
onEventSelected(event);
},
child: Container(
margin: const EdgeInsets.only(bottom: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withAlpha(51)
: AppColors.rouge.withAlpha(26),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.name,
style: TextStyle(
fontSize: 12,
color: isSelected ? Colors.white : AppColors.rouge,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (CalendarUtils.isMultiDayEvent(event))
Text(
'Jour ${CalendarUtils.calculateDayNumber(event.startDateTime, event.startDateTime)}/${CalendarUtils.calculateTotalDays(event)}',
style: TextStyle(
fontSize: 10,
color: isSelected ? Colors.white : AppColors.rouge,
),
maxLines: 1,
),
],
),
),
);
}
}

View File

@ -0,0 +1,383 @@
import 'package:flutter/material.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/calendar_utils.dart';
class WeekView extends StatelessWidget {
final DateTime focusedDay;
final List<EventModel> events;
final Function(int) onWeekChange;
final Function(EventModel) onEventSelected;
final Function() onSwitchToMonth;
const WeekView({
super.key,
required this.focusedDay,
required this.events,
required this.onWeekChange,
required this.onEventSelected,
required this.onSwitchToMonth,
});
@override
Widget build(BuildContext context) {
final weekStart =
focusedDay.subtract(Duration(days: focusedDay.weekday - 1));
final weekEnd = weekStart.add(const Duration(days: 6));
return Column(
children: [
_buildWeekHeader(weekStart, weekEnd),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final availableHeight = constraints.maxHeight - 80;
final hourHeight = availableHeight / 24;
final dayWidth = (constraints.maxWidth - 50) / 7;
return SingleChildScrollView(
child: SizedBox(
height: 24 * hourHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHourColumn(hourHeight),
Expanded(
child: _buildWeekGrid(
weekStart,
hourHeight,
dayWidth,
constraints,
),
),
],
),
),
);
},
),
),
],
);
}
Widget _buildWeekHeader(DateTime weekStart, DateTime weekEnd) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => onWeekChange(-1),
),
Text(
CalendarUtils.getMonthYearString(weekStart, weekEnd),
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Row(
children: [
TextButton(
onPressed: onSwitchToMonth,
style: TextButton.styleFrom(
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
child: const Text('Semaine'),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => onWeekChange(1),
),
],
),
],
),
),
_buildDaysHeader(weekStart),
],
);
}
Widget _buildDaysHeader(DateTime weekStart) {
return SizedBox(
height: 40,
child: Row(
children: [
Container(
width: 50,
color: Colors.transparent,
),
...List.generate(7, (index) {
final day = weekStart.add(Duration(days: index));
return Expanded(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color:
index < 6 ? Colors.grey.shade300 : Colors.transparent,
width: 1,
),
),
),
child: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
CalendarUtils.getShortDayName(day.weekday),
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'${day.day}',
style: const TextStyle(fontSize: 13),
),
],
),
),
if (CalendarUtils.getEventsForDay(day, events).isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: BorderRadius.circular(10),
),
child: Text(
CalendarUtils.getEventsForDay(day, events)
.length
.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}),
],
),
);
}
Widget _buildHourColumn(double hourHeight) {
return Column(
children: List.generate(24, (index) {
return Container(
width: 50,
height: hourHeight,
alignment: Alignment.topRight,
padding: const EdgeInsets.only(right: 4),
child: Text(
'${index.toString().padLeft(2, '0')}:00',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
);
}),
);
}
Widget _buildWeekGrid(
DateTime weekStart,
double hourHeight,
double dayWidth,
BoxConstraints constraints,
) {
final eventsByDay = _prepareEventsByDay(weekStart);
final eventsWithColumnsByDay = _assignColumnsToEvents(eventsByDay);
return Stack(
children: [
// Lignes horizontales
Column(
children: List.generate(24, (index) {
return Container(
height: hourHeight,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.shade300,
width: 0.5,
),
),
),
);
}),
),
// Bordures verticales entre jours
Positioned.fill(
child: Row(
children: List.generate(7, (i) {
return Container(
width: dayWidth,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: i < 6 ? Colors.grey.shade300 : Colors.transparent,
width: 1,
),
),
),
);
}),
),
),
// Événements
...List.generate(7, (dayIdx) {
final dayEvents = eventsWithColumnsByDay[dayIdx];
return Stack(
children: dayEvents.map((e) {
final startHour = e.start.hour + e.start.minute / 60;
final endHour = e.end.hour + e.end.minute / 60;
final duration = endHour - startHour;
final width = dayWidth / e.totalColumns;
return Positioned(
left: dayIdx * dayWidth + e.column * width,
top: startHour * hourHeight,
width: width,
height: duration * hourHeight,
child: GestureDetector(
onTap: () => onEventSelected(e.event),
child: Container(
margin: const EdgeInsets.all(2),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.rouge.withAlpha(26),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.event.name,
style: const TextStyle(
color: AppColors.rouge,
fontSize: 12,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (CalendarUtils.isMultiDayEvent(e.event))
Text(
'Jour ${CalendarUtils.calculateDayNumber(e.event.startDateTime, weekStart.add(Duration(days: dayIdx)))}/${CalendarUtils.calculateTotalDays(e.event)}',
style: const TextStyle(
color: AppColors.rouge,
fontSize: 10,
),
maxLines: 1,
),
],
),
),
),
);
}).toList(),
);
}),
],
);
}
List<List<_PositionedEvent>> _prepareEventsByDay(DateTime weekStart) {
List<List<_PositionedEvent>> eventsByDay = List.generate(7, (i) => []);
for (final event in events) {
for (int i = 0; i < 7; i++) {
final day = weekStart.add(Duration(days: i));
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
if (!(event.endDateTime.isBefore(dayStart) ||
event.startDateTime.isAfter(dayEnd))) {
final start = event.startDateTime.isBefore(dayStart)
? dayStart
: event.startDateTime;
final end =
event.endDateTime.isAfter(dayEnd) ? dayEnd : event.endDateTime;
eventsByDay[i].add(_PositionedEvent(event, start, end));
}
}
}
return eventsByDay;
}
List<List<_PositionedEventWithColumn>> _assignColumnsToEvents(
List<List<_PositionedEvent>> eventsByDay) {
return eventsByDay.map((dayEvents) {
dayEvents.sort((a, b) => a.start.compareTo(b.start));
List<_PositionedEventWithColumn> result = [];
List<List<_PositionedEventWithColumn>> columns = [];
for (final e in dayEvents) {
bool placed = false;
for (int col = 0; col < columns.length; col++) {
if (columns[col].isEmpty || !_overlap(columns[col].last, e)) {
columns[col].add(
_PositionedEventWithColumn(e.event, e.start, e.end, col, 0));
placed = true;
break;
}
}
if (!placed) {
columns.add([
_PositionedEventWithColumn(
e.event, e.start, e.end, columns.length, 0)
]);
}
}
int totalCols = columns.length;
for (final col in columns) {
for (final e in col) {
result.add(_PositionedEventWithColumn(
e.event, e.start, e.end, e.column, totalCols));
}
}
return result;
}).toList();
}
bool _overlap(_PositionedEvent a, _PositionedEvent b) {
return a.end.isAfter(b.start) && a.start.isBefore(b.end);
}
}
class _PositionedEvent {
final EventModel event;
final DateTime start;
final DateTime end;
_PositionedEvent(this.event, this.start, this.end);
}
class _PositionedEventWithColumn extends _PositionedEvent {
final int column;
final int totalColumns;
_PositionedEventWithColumn(
super.event, super.start, super.end, this.column, this.totalColumns);
}

View File

@ -7,7 +7,7 @@ class BigLeftImageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.gris.withOpacity(0.1),
color: AppColors.gris.withAlpha(26),
child: ClipRRect(
borderRadius: BorderRadius.zero,
child: Image.asset(

View File

@ -56,7 +56,7 @@ class ProfilePictureWidget extends StatelessWidget {
child: SizedBox(
width: radius * 0.8, // Ajuster la taille du loader
height: radius * 0.8,
child: CircularProgressIndicator(
child: const CircularProgressIndicator(
strokeWidth: 2), // Indicateur de chargement
),
);

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/views/widgets/image/profile_picture.dart';
class ProfilePictureSelector extends StatefulWidget {
const ProfilePictureSelector({super.key});
@override
State<ProfilePictureSelector> createState() => _ProfilePictureSelectorState();
}
class _ProfilePictureSelectorState extends State<ProfilePictureSelector> {
bool _isHovering = false;
Future<void> _pickAndUploadImage() async {
if (!context.mounted) return;
final provider = Provider.of<LocalUserProvider>(context, listen: false);
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null && context.mounted) {
await provider.changeProfilePicture(image);
}
}
@override
Widget build(BuildContext context) {
final userProvider = Provider.of<LocalUserProvider>(context);
final String userId = userProvider.uid ?? '';
return MouseRegion(
onEnter: (_) => setState(() => _isHovering = true),
onExit: (_) => setState(() => _isHovering = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: _pickAndUploadImage,
child: Stack(
alignment: Alignment.center,
children: [
ProfilePictureWidget(userId: userId, radius: 80),
if (_isHovering)
Container(
width: 160,
height: 160,
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.edit,
size: 36,
color: Colors.white,
),
),
),
],
),
),
);
}
}

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';
@ -6,15 +6,20 @@ 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/utils/permission_gate.dart';
import 'package:em2rp/models/role_model.dart';
class MainDrawer extends StatelessWidget {
final String currentPage;
const MainDrawer({super.key, required this.currentPage});
const MainDrawer({
super.key,
required this.currentPage,
});
@override
Widget build(BuildContext context) {
return Consumer<LocalAuthProvider>(
return Consumer<LocalUserProvider>(
builder: (context, userProvider, child) {
final hasUser = userProvider.currentUser != null;
@ -25,10 +30,10 @@ class MainDrawer extends StatelessWidget {
DrawerHeader(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/EM2_NsurB.jpg'),
image: const AssetImage('assets/EM2_NsurB.jpg'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
AppColors.noir.withOpacity(0.4),
AppColors.noir.withAlpha(102),
BlendMode.darken,
),
),
@ -47,7 +52,7 @@ class MainDrawer extends StatelessWidget {
radius: 30,
)
else
CircleAvatar(
const CircleAvatar(
radius: 30,
child: Icon(Icons.account_circle, size: 45),
),
@ -56,7 +61,7 @@ class MainDrawer extends StatelessWidget {
hasUser
? 'Bonjour, ${userProvider.currentUser!.firstName}'
: 'Bonjour, Utilisateur',
style: TextStyle(
style: const TextStyle(
color: AppColors.blanc,
fontSize: 18,
fontWeight: FontWeight.bold,
@ -83,7 +88,8 @@ class MainDrawer extends StatelessWidget {
Navigator.pop(context);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => CalendarPage()),
MaterialPageRoute(
builder: (context) => const CalendarPage()),
);
},
),
@ -110,19 +116,23 @@ class MainDrawer extends StatelessWidget {
);
},
),
ListTile(
leading: const Icon(Icons.group),
title: const Text('Gestion des Utilisateurs'),
selected: currentPage == '/user_management',
selectedColor: AppColors.rouge,
onTap: () {
Navigator.pop(context);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const UserManagementPage()),
);
},
PermissionGate(
requiredPermissions: const [Permission.viewUsers],
child: ListTile(
leading: const Icon(Icons.group),
title: const Text('Gestion des Utilisateurs'),
selected: currentPage == '/user_management',
selectedColor: AppColors.rouge,
onTap: () {
Navigator.pop(context);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
const UserManagementPage()),
);
},
),
),
],
),

View File

@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/providers/users_provider.dart';
import 'package:em2rp/utils/colors.dart';
class EditUserDialog extends StatefulWidget {
final UserModel user;
const EditUserDialog({required this.user, super.key});
@override
State<EditUserDialog> createState() => _EditUserDialogState();
}
class _EditUserDialogState extends State<EditUserDialog> {
late final TextEditingController firstNameController;
late final TextEditingController lastNameController;
late final TextEditingController emailController;
late final TextEditingController phoneController;
String selectedRole = '';
static const List<String> roles = ['ADMIN', 'CREW'];
@override
void initState() {
super.initState();
firstNameController = TextEditingController(text: widget.user.firstName);
lastNameController = TextEditingController(text: widget.user.lastName);
emailController = TextEditingController(text: widget.user.email);
phoneController = TextEditingController(text: widget.user.phoneNumber);
selectedRole = widget.user.role.isEmpty ? roles.first : widget.user.role;
}
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
super.dispose();
}
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),
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: 400,
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Icons.edit, color: AppColors.rouge),
const SizedBox(width: 12),
Text(
'Modifier 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<String>(
value: selectedRole,
decoration: _buildInputDecoration(
'Rôle', Icons.admin_panel_settings_outlined),
items: roles.map((String role) {
return DropdownMenuItem<String>(
value: role,
child: Text(role),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
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: () {
final updatedUser = widget.user.copyWith(
firstName: firstNameController.text,
lastName: lastNameController.text,
email: emailController.text,
phoneNumber: phoneController.text,
role: selectedRole,
);
Provider.of<UsersProvider>(context, listen: false)
.updateUser(updatedUser);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Enregistrer',
style: TextStyle(color: AppColors.blanc),
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/utils/colors.dart';
class UserCard extends StatelessWidget {
final UserModel user;
final VoidCallback onEdit;
final VoidCallback onDelete;
static const double _desktopMaxWidth = 280;
const UserCard({
super.key,
required this.user,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isMobile = width < 600;
return Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3,
child: Container(
constraints: BoxConstraints(
maxWidth: isMobile ? double.infinity : _desktopMaxWidth,
),
padding: const EdgeInsets.all(12),
child:
isMobile ? _buildMobileRow(context) : _buildDesktopColumn(context),
),
);
}
Widget _buildMobileRow(BuildContext context) {
return Row(
children: [
_profileAvatar(40),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${user.firstName} ${user.lastName}",
style: Theme.of(context).textTheme.titleSmall,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
user.email,
style: Theme.of(context).textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: onEdit,
color: AppColors.rouge,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
IconButton(
icon: const Icon(Icons.delete, size: 20),
onPressed: onDelete,
color: AppColors.gris,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
],
),
],
);
}
Widget _buildDesktopColumn(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth < 200;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
_profileAvatar(48),
const SizedBox(height: 12),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${user.firstName} ${user.lastName}",
style: Theme.of(context).textTheme.titleSmall,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
user.email,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
if (user.role.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
user.role,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: AppColors.gris,
fontSize: 11,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
],
],
),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: isNarrow
? Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildButton(
icon: Icons.edit,
label: "Modifier",
onPressed: onEdit,
color: AppColors.rouge,
isNarrow: true,
),
const SizedBox(height: 4),
_buildButton(
icon: Icons.delete,
label: "Supprimer",
onPressed: onDelete,
color: AppColors.gris,
isNarrow: true,
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildButton(
icon: Icons.edit,
label: "Modifier",
onPressed: onEdit,
color: AppColors.rouge,
isNarrow: false,
),
const SizedBox(width: 8),
_buildButton(
icon: Icons.delete,
label: "Supprimer",
onPressed: onDelete,
color: AppColors.gris,
isNarrow: false,
),
],
),
),
],
);
},
);
}
Widget _buildButton({
required IconData icon,
required String label,
required VoidCallback onPressed,
required Color color,
required bool isNarrow,
}) {
return SizedBox(
height: 26,
width: isNarrow ? double.infinity : null,
child: ElevatedButton.icon(
icon: Icon(icon, size: 14),
label: Text(
label,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: AppColors.blanc,
padding: EdgeInsets.symmetric(
horizontal: isNarrow ? 4 : 8,
vertical: 0,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
);
}
Widget _profileAvatar(double size) {
return CircleAvatar(
radius: size / 2,
backgroundImage: user.profilePhotoUrl.isNotEmpty
? NetworkImage(user.profilePhotoUrl)
: null,
backgroundColor: Colors.grey[200],
child: user.profilePhotoUrl.isEmpty
? Icon(Icons.person, size: size * 0.6, color: AppColors.noir)
: null,
);
}
}

View File

@ -0,0 +1,71 @@
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 StatefulWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final bool showLogoutButton;
const CustomAppBar({
super.key,
required this.title,
this.actions,
this.showLogoutButton = true,
});
@override
State<CustomAppBar> createState() => _CustomAppBarState();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _CustomAppBarState extends State<CustomAppBar> {
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(widget.title),
backgroundColor: AppColors.rouge,
actions: [
if (widget.showLogoutButton)
IconButton(
icon: const Icon(Icons.logout, color: AppColors.blanc),
onPressed: () async {
// Afficher une boîte de dialogue de confirmation
final shouldLogout = await showDialog<bool>(
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 && context.mounted) {
// Déconnexion
final provider =
Provider.of<LocalUserProvider>(context, listen: false);
await provider.signOut();
if (context.mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
}
},
),
if (widget.actions != null) ...widget.actions!,
],
);
}
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:intl/intl.dart';
class EventDetails extends StatelessWidget {
final EventModel event;
final DateTime? selectedDate;
final List<EventModel> events;
final void Function(EventModel, DateTime) onSelectEvent;
const EventDetails({
super.key,
required this.event,
required this.selectedDate,
required this.events,
required this.onSelectEvent,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '');
final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR');
// Trie les événements par date de début
final sortedEvents = List<EventModel>.from(events)
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id);
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: currentIndex > 0
? () {
final prevEvent = sortedEvents[currentIndex - 1];
onSelectEvent(prevEvent, prevEvent.startDateTime);
}
: null,
icon: const Icon(Icons.arrow_back),
color: AppColors.rouge,
),
if (selectedDate != null)
Expanded(
child: Center(
child: Text(
fullDateFormat.format(selectedDate!),
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.rouge,
fontWeight: FontWeight.bold,
),
),
),
),
IconButton(
onPressed: currentIndex < sortedEvents.length - 1
? () {
final nextEvent = sortedEvents[currentIndex + 1];
onSelectEvent(nextEvent, nextEvent.startDateTime);
}
: null,
icon: const Icon(Icons.arrow_forward),
color: AppColors.rouge,
),
],
),
const SizedBox(height: 16),
Text(
event.name,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow(
context,
Icons.calendar_today,
'Date de début',
dateFormat.format(event.startDateTime),
),
_buildInfoRow(
context,
Icons.calendar_today,
'Date de fin',
dateFormat.format(event.endDateTime),
),
_buildInfoRow(
context,
Icons.euro,
'Prix',
currencyFormat.format(event.price),
),
_buildInfoRow(
context,
Icons.build,
'Temps d\'installation',
'${event.installationTime} heures',
),
_buildInfoRow(
context,
Icons.construction,
'Temps de démontage',
'${event.disassemblyTime} heures',
),
const SizedBox(height: 16),
Text(
'Description',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
event.description,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
Text(
'Adresse',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'${event.address.latitude}° N, ${event.address.longitude}° E',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
);
}
Widget _buildInfoRow(
BuildContext context,
IconData icon,
String label,
String value,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(icon, color: AppColors.rouge),
const SizedBox(width: 8),
Text(
'$label : ',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
Text(
value,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}
}

View File

@ -1,7 +1,7 @@
name: em2rp
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
version: 1.0.0+1
environment:
sdk: ^3.5.4
@ -9,6 +9,7 @@ environment:
dependencies:
flutter:
sdk: flutter
firebase_core: ^3.12.1
firebase_auth: ^5.5.1
cloud_firestore: ^5.6.5
@ -17,6 +18,40 @@ dependencies:
firebase_storage: ^12.4.4
image_picker: ^1.1.2
universal_io: ^2.2.2
cupertino_icons: ^1.0.2
table_calendar: ^3.0.9
intl: ^0.19.0
google_maps_flutter: ^2.5.0
permission_handler: ^11.1.0
geolocator: ^10.1.0
flutter_map: ^6.1.0
latlong2: ^0.9.0
flutter_launcher_icons: ^0.13.1
flutter_native_splash: ^2.3.9
url_launcher: ^6.2.2
share_plus: ^7.2.1
path_provider: ^2.1.2
pdf: ^3.10.7
printing: ^5.11.1
flutter_local_notifications: ^16.3.0
timezone: ^0.9.2
flutter_secure_storage: ^9.0.0
http: ^1.1.2
flutter_dotenv: ^5.1.0
google_fonts: ^6.1.0
flutter_svg: ^2.0.9
cached_network_image: ^3.3.1
flutter_staggered_grid_view: ^0.7.0
shimmer: ^3.0.0
flutter_slidable: ^3.0.1
flutter_datetime_picker: ^1.5.1
flutter_colorpicker: ^1.0.3
flutter_rating_bar: ^4.0.1
flutter_chat_ui: ^1.6.10
flutter_chat_types: ^3.6.2
uuid: ^4.2.2
flutter_localizations:
sdk: flutter
dev_dependencies:
flutter_test: