Compare commits
10 Commits
2b8e7085aa
...
64e5eddfd5
Author | SHA1 | Date | |
---|---|---|---|
64e5eddfd5 | |||
6adc90ecfe | |||
62c6125d8c | |||
8bd80dc7f3 | |||
72bb8f03de | |||
b8e4f39e4c | |||
6c158aa6cb | |||
94337581d8 | |||
4c7ce27a0c | |||
456d0bb4b8 |
90
em2rp/firestore.rules
Normal file
90
em2rp/firestore.rules
Normal 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
17
em2rp/lib/config/env.dart
Normal 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
|
||||||
|
}
|
@ -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/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/calendar_page.dart';
|
||||||
import 'package:em2rp/views/login_page.dart';
|
import 'package:em2rp/views/login_page.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
@ -10,9 +11,10 @@ import 'utils/colors.dart';
|
|||||||
import 'views/my_account_page.dart';
|
import 'views/my_account_page.dart';
|
||||||
import 'views/user_management_page.dart';
|
import 'views/user_management_page.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'providers/user_provider.dart';
|
import 'providers/local_user_provider.dart';
|
||||||
import 'providers/local_auth_provider.dart'; // Ajout de l'AuthProvider
|
|
||||||
import 'services/user_service.dart';
|
import 'services/user_service.dart';
|
||||||
|
import 'pages/auth/reset_password_page.dart';
|
||||||
|
import 'config/env.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -27,18 +29,18 @@ void main() async {
|
|||||||
// Injection du service UserService
|
// Injection du service UserService
|
||||||
Provider<UserService>(create: (_) => UserService()),
|
Provider<UserService>(create: (_) => UserService()),
|
||||||
|
|
||||||
// AuthProvider pour la gestion de l'authentification
|
// LocalUserProvider pour la gestion de l'authentification
|
||||||
ChangeNotifierProvider<LocalAuthProvider>(
|
ChangeNotifierProvider<LocalUserProvider>(
|
||||||
create: (context) => LocalAuthProvider()),
|
create: (context) => LocalUserProvider()),
|
||||||
|
|
||||||
// UserProvider déjà existant
|
// Injection des Providers en utilisant UserService
|
||||||
ChangeNotifierProvider<UserProvider>(
|
ChangeNotifierProvider<UsersProvider>(
|
||||||
create: (context) => UserProvider()),
|
create: (context) => UsersProvider(context.read<UserService>()),
|
||||||
|
),
|
||||||
|
|
||||||
// Injection des ViewModels en utilisant UserService et AuthProvider
|
// EventProvider pour la gestion des événements
|
||||||
ChangeNotifierProvider<UserManagementViewModel>(
|
ChangeNotifierProvider<EventProvider>(
|
||||||
create: (context) =>
|
create: (context) => EventProvider(),
|
||||||
UserManagementViewModel(context.read<UserService>()),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
@ -62,7 +64,7 @@ class MyApp extends StatelessWidget {
|
|||||||
textTheme: const TextTheme(
|
textTheme: const TextTheme(
|
||||||
bodyMedium: TextStyle(color: AppColors.noir),
|
bodyMedium: TextStyle(color: AppColors.noir),
|
||||||
),
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: AppColors.noir),
|
borderSide: BorderSide(color: AppColors.noir),
|
||||||
),
|
),
|
||||||
@ -79,14 +81,74 @@ class MyApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
home: const AutoLoginWrapper(),
|
||||||
routes: {
|
routes: {
|
||||||
'/login': (context) => const LoginPage(),
|
'/login': (context) => const LoginPage(),
|
||||||
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
||||||
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
||||||
'/user_management': (context) =>
|
'/user_management': (context) =>
|
||||||
const AuthGuard(requiredRole: "ADMIN", child: UserManagementPage()),
|
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(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
82
em2rp/lib/models/event_model.dart
Normal file
82
em2rp/lib/models/event_model.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
100
em2rp/lib/models/role_model.dart
Normal file
100
em2rp/lib/models/role_model.dart
Normal 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];
|
||||||
|
}
|
@ -41,4 +41,23 @@ class UserModel {
|
|||||||
'phoneNumber': phoneNumber,
|
'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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
147
em2rp/lib/pages/auth/reset_password_page.dart
Normal file
147
em2rp/lib/pages/auth/reset_password_page.dart
Normal 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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
105
em2rp/lib/providers/event_provider.dart
Normal file
105
em2rp/lib/providers/event_provider.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
157
em2rp/lib/providers/local_user_provider.dart
Normal file
157
em2rp/lib/providers/local_user_provider.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
166
em2rp/lib/providers/users_provider.dart
Normal file
166
em2rp/lib/providers/users_provider.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/user_provider.dart';
|
|
||||||
import 'package:em2rp/views/login_page.dart';
|
import 'package:em2rp/views/login_page.dart';
|
||||||
|
|
||||||
class AuthGuard extends StatelessWidget {
|
class AuthGuard extends StatelessWidget {
|
||||||
@ -17,7 +16,7 @@ class AuthGuard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localAuthProvider = Provider.of<LocalAuthProvider>(context);
|
final localAuthProvider = Provider.of<LocalUserProvider>(context);
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas connecté
|
// Si l'utilisateur n'est pas connecté
|
||||||
if (localAuthProvider.currentUser == null) {
|
if (localAuthProvider.currentUser == null) {
|
||||||
|
115
em2rp/lib/utils/calendar_utils.dart
Normal file
115
em2rp/lib/utils/calendar_utils.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
42
em2rp/lib/utils/permission_gate.dart
Normal file
42
em2rp/lib/utils/permission_gate.dart
Normal 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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.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';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class LoginViewModel extends ChangeNotifier {
|
class LoginViewModel extends ChangeNotifier {
|
||||||
@ -19,7 +19,7 @@ class LoginViewModel extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> signIn(BuildContext context) async {
|
Future<void> signIn(BuildContext context) async {
|
||||||
final localAuthProvider =
|
final localAuthProvider =
|
||||||
Provider.of<LocalAuthProvider>(context, listen: false);
|
Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
highlightPasswordField = false;
|
highlightPasswordField = false;
|
||||||
@ -27,23 +27,32 @@ class LoginViewModel extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await localAuthProvider.signInWithEmailAndPassword(
|
|
||||||
emailController.text.trim(), passwordController.text);
|
|
||||||
print('User signed in');
|
print('User signed in');
|
||||||
|
|
||||||
|
// Attendre que les données utilisateur soient chargées
|
||||||
|
await localAuthProvider.loadUserData();
|
||||||
|
|
||||||
// Vérifier si le contexte est toujours valide
|
// Vérifier si le contexte est toujours valide
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
// Utiliser pushReplacementNamed pour une transition propre
|
// Vérifier si l'utilisateur a bien été chargé
|
||||||
Navigator.of(context, rootNavigator: true)
|
if (localAuthProvider.currentUser != null) {
|
||||||
.pushReplacementNamed('/calendar');
|
// 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) {
|
} on FirebaseAuthException catch (e) {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
// Gérer les erreurs...
|
errorMessage =
|
||||||
|
e.message ?? 'Une erreur est survenue lors de la connexion';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} finally {
|
} catch (e) {
|
||||||
// S'assurer que isLoading est remis à false même en cas d'erreur inattendue
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
errorMessage = 'Une erreur inattendue est survenue';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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:flutter/material.dart';
|
||||||
|
import 'package:em2rp/widgets/custom_app_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
import 'package:provider/provider.dart'; // Import Provider
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/user_provider.dart'; // Import UserProvider
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
import 'package:em2rp/utils/colors.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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Calendrier')),
|
appBar: const CustomAppBar(
|
||||||
drawer: MainDrawer(
|
title: 'Calendrier',
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,240 +1,96 @@
|
|||||||
import 'package:em2rp/providers/local_auth_provider.dart';
|
import 'package:em2rp/providers/local_user_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/views/widgets/nav/main_drawer.dart';
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
import 'package:flutter/material.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: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/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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localAuthProvider = Provider.of<LocalAuthProvider>(
|
|
||||||
context,
|
|
||||||
); // Get UserProvider instance
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar:
|
appBar: const CustomAppBar(
|
||||||
AppBar(title: const Text('Mon Compte')), // More user-friendly title
|
title: 'Mon compte',
|
||||||
drawer: MainDrawer(
|
),
|
||||||
currentPage: '/my_account'), // Pass UserProvider to MainDrawer
|
drawer: const MainDrawer(currentPage: '/my_account'),
|
||||||
body: SingleChildScrollView(
|
body: Consumer<LocalUserProvider>(
|
||||||
// Added SingleChildScrollView for better responsiveness
|
builder: (context, userProvider, child) {
|
||||||
child: Padding(
|
final user = userProvider.currentUser;
|
||||||
padding: const EdgeInsets.all(
|
|
||||||
24.0), // Increased padding around the main content
|
if (user == null) {
|
||||||
child: Column(
|
return const Center(child: CircularProgressIndicator());
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
}
|
||||||
children: [
|
|
||||||
MouseRegion(
|
final firstNameController =
|
||||||
onEnter: (_) => setState(() => _isHoveringProfilePic = true),
|
TextEditingController(text: user.firstName);
|
||||||
onExit: (_) => setState(() => _isHoveringProfilePic = false),
|
final lastNameController = TextEditingController(text: user.lastName);
|
||||||
cursor: SystemMouseCursors.click,
|
final phoneController = TextEditingController(text: user.phoneNumber);
|
||||||
child: GestureDetector(
|
|
||||||
onTap:
|
return SingleChildScrollView(
|
||||||
_changeProfilePicture, // Call _changeProfilePicture on tap
|
child: Padding(
|
||||||
child: Stack(
|
padding: const EdgeInsets.all(24.0),
|
||||||
alignment: Alignment.center,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
ProfilePictureWidget(
|
children: [
|
||||||
userId: user!.uid,
|
const ProfilePictureSelector(),
|
||||||
radius:
|
Center(
|
||||||
80), // Increased radius for larger profile picture
|
child: Card(
|
||||||
if (_isHoveringProfilePic)
|
elevation: 4.0,
|
||||||
Container(
|
shape: RoundedRectangleBorder(
|
||||||
width: 160, // Slightly larger hover overlay
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
height: 160,
|
),
|
||||||
decoration: BoxDecoration(
|
child: Padding(
|
||||||
color: Colors.black54,
|
padding: const EdgeInsets.all(24.0),
|
||||||
shape: BoxShape.circle,
|
child: ConstrainedBox(
|
||||||
),
|
constraints: const BoxConstraints(maxWidth: 500),
|
||||||
child: const Center(
|
child: Column(
|
||||||
child: Icon(
|
children: [
|
||||||
Icons.edit,
|
StyledTextField(
|
||||||
color: Colors.white,
|
labelText: 'Prénom',
|
||||||
size: 36, // Slightly larger edit icon
|
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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:flutter/material.dart';
|
||||||
import 'package:provider/provider.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});
|
const UserManagementPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<UserManagementPage> createState() => _UserManagementPageState();
|
||||||
final userViewModel = Provider.of<UserManagementViewModel>(context);
|
}
|
||||||
final authProvider = Provider.of<LocalAuthProvider>(context);
|
|
||||||
|
|
||||||
if (authProvider.role != 'ADMIN') {
|
class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
return Scaffold(
|
@override
|
||||||
appBar: AppBar(title: const Text('Gestion des Utilisateurs')),
|
void initState() {
|
||||||
body: const Center(
|
super.initState();
|
||||||
child: Text('Accès non autorisé',
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
style: TextStyle(color: Colors.red))),
|
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(
|
showDialog(
|
||||||
appBar: AppBar(title: const Text('Gestion des Utilisateurs')),
|
context: context,
|
||||||
body: userViewModel.isLoading
|
builder: (context) => Dialog(
|
||||||
? const Center(child: CircularProgressIndicator())
|
shape: RoundedRectangleBorder(
|
||||||
: ListView.builder(
|
borderRadius: BorderRadius.circular(16),
|
||||||
itemCount: userViewModel.users.length,
|
),
|
||||||
itemBuilder: (context, index) {
|
child: Container(
|
||||||
final user = userViewModel.users[index];
|
width: 400,
|
||||||
return ListTile(
|
padding: const EdgeInsets.all(24),
|
||||||
leading: CircleAvatar(
|
child: Column(
|
||||||
backgroundImage: NetworkImage(user.profilePhotoUrl)),
|
mainAxisSize: MainAxisSize.min,
|
||||||
title: Text('${user.firstName} ${user.lastName}'),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
subtitle: Text(user.email),
|
children: [
|
||||||
trailing: Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
const Icon(Icons.person_add, color: AppColors.rouge),
|
||||||
IconButton(
|
const SizedBox(width: 12),
|
||||||
icon: const Icon(Icons.edit),
|
Text(
|
||||||
onPressed: () {
|
'Nouvel utilisateur',
|
||||||
// Afficher la pop-up d'édition
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
},
|
color: AppColors.noir,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
floatingActionButton: FloatingActionButton(
|
SingleChildScrollView(
|
||||||
onPressed: () {
|
child: Column(
|
||||||
// Ajouter un utilisateur
|
mainAxisSize: MainAxisSize.min,
|
||||||
},
|
children: [
|
||||||
child: const Icon(Icons.add),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ class WelcomeTextWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Text(
|
return const Text(
|
||||||
'Bienvenue !',
|
'Bienvenue !',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
257
em2rp/lib/views/widgets/calendar_widgets/month_view.dart
Normal file
257
em2rp/lib/views/widgets/calendar_widgets/month_view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
383
em2rp/lib/views/widgets/calendar_widgets/week_view.dart
Normal file
383
em2rp/lib/views/widgets/calendar_widgets/week_view.dart
Normal 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);
|
||||||
|
}
|
@ -7,7 +7,7 @@ class BigLeftImageWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: AppColors.gris.withOpacity(0.1),
|
color: AppColors.gris.withAlpha(26),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
|
@ -56,7 +56,7 @@ class ProfilePictureWidget extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: radius * 0.8, // Ajuster la taille du loader
|
width: radius * 0.8, // Ajuster la taille du loader
|
||||||
height: radius * 0.8,
|
height: radius * 0.8,
|
||||||
child: CircularProgressIndicator(
|
child: const CircularProgressIndicator(
|
||||||
strokeWidth: 2), // Indicateur de chargement
|
strokeWidth: 2), // Indicateur de chargement
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
63
em2rp/lib/views/widgets/image/profile_picture_selector.dart
Normal file
63
em2rp/lib/views/widgets/image/profile_picture_selector.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/utils/colors.dart';
|
||||||
import 'package:em2rp/views/calendar_page.dart';
|
import 'package:em2rp/views/calendar_page.dart';
|
||||||
import 'package:em2rp/views/my_account_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:flutter/material.dart';
|
||||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/utils/permission_gate.dart';
|
||||||
|
import 'package:em2rp/models/role_model.dart';
|
||||||
|
|
||||||
class MainDrawer extends StatelessWidget {
|
class MainDrawer extends StatelessWidget {
|
||||||
final String currentPage;
|
final String currentPage;
|
||||||
|
|
||||||
const MainDrawer({super.key, required this.currentPage});
|
const MainDrawer({
|
||||||
|
super.key,
|
||||||
|
required this.currentPage,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<LocalAuthProvider>(
|
return Consumer<LocalUserProvider>(
|
||||||
builder: (context, userProvider, child) {
|
builder: (context, userProvider, child) {
|
||||||
final hasUser = userProvider.currentUser != null;
|
final hasUser = userProvider.currentUser != null;
|
||||||
|
|
||||||
@ -25,10 +30,10 @@ class MainDrawer extends StatelessWidget {
|
|||||||
DrawerHeader(
|
DrawerHeader(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('assets/EM2_NsurB.jpg'),
|
image: const AssetImage('assets/EM2_NsurB.jpg'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
colorFilter: ColorFilter.mode(
|
colorFilter: ColorFilter.mode(
|
||||||
AppColors.noir.withOpacity(0.4),
|
AppColors.noir.withAlpha(102),
|
||||||
BlendMode.darken,
|
BlendMode.darken,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -47,7 +52,7 @@ class MainDrawer extends StatelessWidget {
|
|||||||
radius: 30,
|
radius: 30,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
CircleAvatar(
|
const CircleAvatar(
|
||||||
radius: 30,
|
radius: 30,
|
||||||
child: Icon(Icons.account_circle, size: 45),
|
child: Icon(Icons.account_circle, size: 45),
|
||||||
),
|
),
|
||||||
@ -56,7 +61,7 @@ class MainDrawer extends StatelessWidget {
|
|||||||
hasUser
|
hasUser
|
||||||
? 'Bonjour, ${userProvider.currentUser!.firstName}'
|
? 'Bonjour, ${userProvider.currentUser!.firstName}'
|
||||||
: 'Bonjour, Utilisateur',
|
: 'Bonjour, Utilisateur',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.blanc,
|
color: AppColors.blanc,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -83,7 +88,8 @@ class MainDrawer extends StatelessWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => CalendarPage()),
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const CalendarPage()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -110,19 +116,23 @@ class MainDrawer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
PermissionGate(
|
||||||
leading: const Icon(Icons.group),
|
requiredPermissions: const [Permission.viewUsers],
|
||||||
title: const Text('Gestion des Utilisateurs'),
|
child: ListTile(
|
||||||
selected: currentPage == '/user_management',
|
leading: const Icon(Icons.group),
|
||||||
selectedColor: AppColors.rouge,
|
title: const Text('Gestion des Utilisateurs'),
|
||||||
onTap: () {
|
selected: currentPage == '/user_management',
|
||||||
Navigator.pop(context);
|
selectedColor: AppColors.rouge,
|
||||||
Navigator.pushReplacement(
|
onTap: () {
|
||||||
context,
|
Navigator.pop(context);
|
||||||
MaterialPageRoute(
|
Navigator.pushReplacement(
|
||||||
builder: (context) => const UserManagementPage()),
|
context,
|
||||||
);
|
MaterialPageRoute(
|
||||||
},
|
builder: (context) =>
|
||||||
|
const UserManagementPage()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
184
em2rp/lib/views/widgets/user_management/edit_user_dialog.dart
Normal file
184
em2rp/lib/views/widgets/user_management/edit_user_dialog.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
232
em2rp/lib/views/widgets/user_management/user_card.dart
Normal file
232
em2rp/lib/views/widgets/user_management/user_card.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
71
em2rp/lib/widgets/custom_app_bar.dart
Normal file
71
em2rp/lib/widgets/custom_app_bar.dart
Normal 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!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
172
em2rp/lib/widgets/event_details.dart
Normal file
172
em2rp/lib/widgets/event_details.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
name: em2rp
|
name: em2rp
|
||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.1.0
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@ -9,6 +9,7 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
firebase_core: ^3.12.1
|
firebase_core: ^3.12.1
|
||||||
firebase_auth: ^5.5.1
|
firebase_auth: ^5.5.1
|
||||||
cloud_firestore: ^5.6.5
|
cloud_firestore: ^5.6.5
|
||||||
@ -17,6 +18,40 @@ dependencies:
|
|||||||
firebase_storage: ^12.4.4
|
firebase_storage: ^12.4.4
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
universal_io: ^2.2.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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user