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/view_model/user_management_view_model.dart';
|
||||
import 'package:em2rp/views/calendar_page.dart';
|
||||
import 'package:em2rp/views/login_page.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
@ -10,9 +11,10 @@ import 'utils/colors.dart';
|
||||
import 'views/my_account_page.dart';
|
||||
import 'views/user_management_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'providers/user_provider.dart';
|
||||
import 'providers/local_auth_provider.dart'; // Ajout de l'AuthProvider
|
||||
import 'providers/local_user_provider.dart';
|
||||
import 'services/user_service.dart';
|
||||
import 'pages/auth/reset_password_page.dart';
|
||||
import 'config/env.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@ -27,18 +29,18 @@ void main() async {
|
||||
// Injection du service UserService
|
||||
Provider<UserService>(create: (_) => UserService()),
|
||||
|
||||
// AuthProvider pour la gestion de l'authentification
|
||||
ChangeNotifierProvider<LocalAuthProvider>(
|
||||
create: (context) => LocalAuthProvider()),
|
||||
// LocalUserProvider pour la gestion de l'authentification
|
||||
ChangeNotifierProvider<LocalUserProvider>(
|
||||
create: (context) => LocalUserProvider()),
|
||||
|
||||
// UserProvider déjà existant
|
||||
ChangeNotifierProvider<UserProvider>(
|
||||
create: (context) => UserProvider()),
|
||||
// Injection des Providers en utilisant UserService
|
||||
ChangeNotifierProvider<UsersProvider>(
|
||||
create: (context) => UsersProvider(context.read<UserService>()),
|
||||
),
|
||||
|
||||
// Injection des ViewModels en utilisant UserService et AuthProvider
|
||||
ChangeNotifierProvider<UserManagementViewModel>(
|
||||
create: (context) =>
|
||||
UserManagementViewModel(context.read<UserService>()),
|
||||
// EventProvider pour la gestion des événements
|
||||
ChangeNotifierProvider<EventProvider>(
|
||||
create: (context) => EventProvider(),
|
||||
),
|
||||
],
|
||||
child: const MyApp(),
|
||||
@ -62,7 +64,7 @@ class MyApp extends StatelessWidget {
|
||||
textTheme: const TextTheme(
|
||||
bodyMedium: TextStyle(color: AppColors.noir),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: AppColors.noir),
|
||||
),
|
||||
@ -79,14 +81,74 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
home: const AutoLoginWrapper(),
|
||||
routes: {
|
||||
'/login': (context) => const LoginPage(),
|
||||
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
||||
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
||||
'/user_management': (context) =>
|
||||
const AuthGuard(requiredRole: "ADMIN", child: UserManagementPage()),
|
||||
'/reset_password': (context) {
|
||||
final args = ModalRoute.of(context)!.settings.arguments
|
||||
as Map<String, dynamic>;
|
||||
return ResetPasswordPage(
|
||||
email: args['email'] as String,
|
||||
actionCode: args['actionCode'] as String,
|
||||
);
|
||||
},
|
||||
},
|
||||
initialRoute: '/login',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoLoginWrapper extends StatefulWidget {
|
||||
const AutoLoginWrapper({super.key});
|
||||
|
||||
@override
|
||||
State<AutoLoginWrapper> createState() => _AutoLoginWrapperState();
|
||||
}
|
||||
|
||||
class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_autoLogin();
|
||||
}
|
||||
|
||||
Future<void> _autoLogin() async {
|
||||
try {
|
||||
final localAuthProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
|
||||
// Vérifier si l'utilisateur est déjà connecté
|
||||
if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) {
|
||||
// Connexion automatique en mode développement
|
||||
await localAuthProvider.signInWithEmailAndPassword(
|
||||
Env.devAdminEmail,
|
||||
Env.devAdminPassword,
|
||||
);
|
||||
}
|
||||
|
||||
// Charger les données utilisateur
|
||||
await localAuthProvider.loadUserData();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/calendar');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Auto login failed: $e');
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
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,
|
||||
};
|
||||
}
|
||||
|
||||
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:provider/provider.dart';
|
||||
import 'package:em2rp/providers/user_provider.dart';
|
||||
import 'package:em2rp/views/login_page.dart';
|
||||
|
||||
class AuthGuard extends StatelessWidget {
|
||||
@ -17,7 +16,7 @@ class AuthGuard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localAuthProvider = Provider.of<LocalAuthProvider>(context);
|
||||
final localAuthProvider = Provider.of<LocalUserProvider>(context);
|
||||
|
||||
// Si l'utilisateur n'est pas connecté
|
||||
if (localAuthProvider.currentUser == null) {
|
||||
|
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:firebase_auth/firebase_auth.dart';
|
||||
import '../providers/local_auth_provider.dart';
|
||||
import '../providers/local_user_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LoginViewModel extends ChangeNotifier {
|
||||
@ -19,7 +19,7 @@ class LoginViewModel extends ChangeNotifier {
|
||||
|
||||
Future<void> signIn(BuildContext context) async {
|
||||
final localAuthProvider =
|
||||
Provider.of<LocalAuthProvider>(context, listen: false);
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
isLoading = true;
|
||||
errorMessage = '';
|
||||
highlightPasswordField = false;
|
||||
@ -27,23 +27,32 @@ class LoginViewModel extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await localAuthProvider.signInWithEmailAndPassword(
|
||||
emailController.text.trim(), passwordController.text);
|
||||
print('User signed in');
|
||||
|
||||
// Attendre que les données utilisateur soient chargées
|
||||
await localAuthProvider.loadUserData();
|
||||
|
||||
// Vérifier si le contexte est toujours valide
|
||||
if (context.mounted) {
|
||||
// Vérifier si l'utilisateur a bien été chargé
|
||||
if (localAuthProvider.currentUser != null) {
|
||||
// Utiliser pushReplacementNamed pour une transition propre
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pushReplacementNamed('/calendar');
|
||||
} else {
|
||||
errorMessage = 'Erreur lors du chargement des données utilisateur';
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
} on FirebaseAuthException catch (e) {
|
||||
isLoading = false;
|
||||
// Gérer les erreurs...
|
||||
errorMessage =
|
||||
e.message ?? 'Une erreur est survenue lors de la connexion';
|
||||
notifyListeners();
|
||||
} finally {
|
||||
// S'assurer que isLoading est remis à false même en cas d'erreur inattendue
|
||||
} catch (e) {
|
||||
isLoading = false;
|
||||
errorMessage = 'Une erreur inattendue est survenue';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -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:em2rp/widgets/custom_app_bar.dart';
|
||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||
import 'package:provider/provider.dart'; // Import Provider
|
||||
import 'package:em2rp/providers/user_provider.dart'; // Import UserProvider
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/widgets/event_details.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
|
||||
|
||||
class CalendarPage extends StatelessWidget {
|
||||
class CalendarPage extends StatefulWidget {
|
||||
const CalendarPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localAuthProvider = Provider.of<LocalAuthProvider>(context);
|
||||
State<CalendarPage> createState() => _CalendarPageState();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Calendrier')),
|
||||
drawer: MainDrawer(
|
||||
currentPage: '/calendar'), // Pass UserProvider to MainDrawer
|
||||
class _CalendarPageState extends State<CalendarPage> {
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay;
|
||||
EventModel? _selectedEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeDateFormatting('fr_FR', null);
|
||||
Future.microtask(() => _loadEvents());
|
||||
}
|
||||
|
||||
Future<void> _loadEvents() async {
|
||||
final localAuthProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final userId = localAuthProvider.uid;
|
||||
|
||||
if (userId != null) {
|
||||
await eventProvider.loadUserEvents(userId);
|
||||
}
|
||||
}
|
||||
|
||||
void _changeWeek(int delta) {
|
||||
setState(() {
|
||||
_focusedDay = _focusedDay.add(Duration(days: 7 * delta));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
if (eventProvider.isLoading) {
|
||||
return const Scaffold(
|
||||
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)),
|
||||
],
|
||||
),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: const CustomAppBar(
|
||||
title: 'Calendrier',
|
||||
),
|
||||
drawer: const MainDrawer(currentPage: '/calendar'),
|
||||
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopLayout() {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
return Row(
|
||||
children: [
|
||||
// Calendrier (65% de la largeur)
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: _buildCalendar(),
|
||||
),
|
||||
// Détails de l'événement (35% de la largeur)
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: _selectedEvent != null
|
||||
? EventDetails(
|
||||
event: _selectedEvent!,
|
||||
selectedDate: _selectedDay,
|
||||
events: eventProvider.events,
|
||||
onSelectEvent: (event, date) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
_selectedDay = date;
|
||||
});
|
||||
},
|
||||
)
|
||||
: const Center(
|
||||
child:
|
||||
Text('Sélectionnez un événement pour voir les détails'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileLayout() {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
return Column(
|
||||
children: [
|
||||
// Calendrier
|
||||
Expanded(
|
||||
child: _buildCalendar(),
|
||||
),
|
||||
// Détails de l'événement
|
||||
if (_selectedEvent != null)
|
||||
Expanded(
|
||||
child: EventDetails(
|
||||
event: _selectedEvent!,
|
||||
selectedDate: _selectedDay,
|
||||
events: eventProvider.events,
|
||||
onSelectEvent: (event, date) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
_selectedDay = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendar() {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
|
||||
if (_calendarFormat == CalendarFormat.week) {
|
||||
return WeekView(
|
||||
focusedDay: _focusedDay,
|
||||
events: eventProvider.events,
|
||||
onWeekChange: _changeWeek,
|
||||
onEventSelected: (event) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
});
|
||||
},
|
||||
onSwitchToMonth: () {
|
||||
setState(() {
|
||||
_calendarFormat = CalendarFormat.month;
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return MonthView(
|
||||
focusedDay: _focusedDay,
|
||||
selectedDay: _selectedDay,
|
||||
calendarFormat: _calendarFormat,
|
||||
events: eventProvider.events,
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
setState(() {
|
||||
_calendarFormat = format;
|
||||
});
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
setState(() {
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onEventSelected: (event) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,229 +1,83 @@
|
||||
import 'package:em2rp/providers/local_auth_provider.dart';
|
||||
import 'package:em2rp/providers/user_provider.dart';
|
||||
import 'package:em2rp/utils/firebase_storage_manager.dart';
|
||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
|
||||
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
|
||||
import 'package:em2rp/widgets/custom_app_bar.dart';
|
||||
|
||||
class MyAccountPage extends StatefulWidget {
|
||||
class MyAccountPage extends StatelessWidget {
|
||||
const MyAccountPage({super.key});
|
||||
@override
|
||||
_MyAccountPageState createState() => _MyAccountPageState();
|
||||
}
|
||||
|
||||
class _MyAccountPageState extends State<MyAccountPage> {
|
||||
final User? user = FirebaseAuth.instance.currentUser;
|
||||
final TextEditingController _firstNameController = TextEditingController();
|
||||
final TextEditingController _lastNameController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
String? profilePhotoUrl;
|
||||
bool _isHoveringProfilePic = false;
|
||||
final FirebaseStorageManager _storageManager =
|
||||
FirebaseStorageManager(); // Instance of FirebaseStorageManager
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserData();
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
if (user != null) {
|
||||
DocumentSnapshot userData = await FirebaseFirestore.instance
|
||||
.collection('users')
|
||||
.doc(user!.uid)
|
||||
.get();
|
||||
|
||||
if (userData.exists) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_firstNameController.text = userData['firstName'] ?? '';
|
||||
_lastNameController.text = userData['lastName'] ?? '';
|
||||
_phoneController.text = userData['phone'] ?? '';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateUserData() async {
|
||||
if (user != null) {
|
||||
try {
|
||||
await FirebaseFirestore.instance.collection('users').doc(user!.uid).set(
|
||||
{
|
||||
'firstName': _firstNameController.text,
|
||||
'lastName': _lastNameController.text,
|
||||
'phone': _phoneController.text,
|
||||
},
|
||||
SetOptions(merge: true),
|
||||
);
|
||||
|
||||
// **MISE À JOUR DU USERPROVIDER APRÈS SUCCÈS**
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
userProvider.setUserFirstName(_firstNameController.text);
|
||||
userProvider.setUserLastName(_lastNameController.text);
|
||||
userProvider.setUserPhoneNumber(_phoneController.text);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Informations personnelles mises à jour avec succès!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Erreur lors de la mise à jour des informations personnelles: ${e.toString()}',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
print(
|
||||
'Erreur lors de la mise à jour des informations utilisateur: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeProfilePicture() async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
); // You can also use ImageSource.camera
|
||||
|
||||
if (image != null) {
|
||||
// Call FirebaseStorageManager to send the profile picture
|
||||
String? newProfilePhotoUrl = await _storageManager.sendProfilePicture(
|
||||
imageFile: image,
|
||||
uid: user!.uid,
|
||||
);
|
||||
if (newProfilePhotoUrl != null) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
profilePhotoUrl =
|
||||
newProfilePhotoUrl; // Update the profilePhotoUrl to refresh the UI
|
||||
});
|
||||
// Optionally, update the UserProvider if you are using it to manage user data globally
|
||||
// Provider.of<UserProvider>(context, listen: false).setUserProfilePhotoUrl(newProfilePhotoUrl);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Photo de profil mise à jour avec succès!'),
|
||||
backgroundColor: Colors.green, // Optional: Style for success
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Erreur lors de la mise à jour de la photo de profil.',
|
||||
),
|
||||
backgroundColor: Colors.red, // Optional: Style for error
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// User cancelled image picking
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Sélection de photo annulée.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localAuthProvider = Provider.of<LocalAuthProvider>(
|
||||
context,
|
||||
); // Get UserProvider instance
|
||||
|
||||
return Scaffold(
|
||||
appBar:
|
||||
AppBar(title: const Text('Mon Compte')), // More user-friendly title
|
||||
drawer: MainDrawer(
|
||||
currentPage: '/my_account'), // Pass UserProvider to MainDrawer
|
||||
body: SingleChildScrollView(
|
||||
// Added SingleChildScrollView for better responsiveness
|
||||
appBar: const CustomAppBar(
|
||||
title: 'Mon compte',
|
||||
),
|
||||
drawer: const MainDrawer(currentPage: '/my_account'),
|
||||
body: Consumer<LocalUserProvider>(
|
||||
builder: (context, userProvider, child) {
|
||||
final user = userProvider.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final firstNameController =
|
||||
TextEditingController(text: user.firstName);
|
||||
final lastNameController = TextEditingController(text: user.lastName);
|
||||
final phoneController = TextEditingController(text: user.phoneNumber);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
24.0), // Increased padding around the main content
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHoveringProfilePic = true),
|
||||
onExit: (_) => setState(() => _isHoveringProfilePic = false),
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap:
|
||||
_changeProfilePicture, // Call _changeProfilePicture on tap
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
userId: user!.uid,
|
||||
radius:
|
||||
80), // Increased radius for larger profile picture
|
||||
if (_isHoveringProfilePic)
|
||||
Container(
|
||||
width: 160, // Slightly larger hover overlay
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color: Colors.white,
|
||||
size: 36, // Slightly larger edit icon
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const ProfilePictureSelector(),
|
||||
Center(
|
||||
child: Card(
|
||||
elevation: 4.0, // Ajouter un léger relief
|
||||
elevation: 4.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12.0)), // Bords arrondis
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
24.0), // Padding intérieur de la carte
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: ConstrainedBox(
|
||||
// Limiter la largeur des inputs dans la carte
|
||||
constraints: BoxConstraints(
|
||||
maxWidth:
|
||||
500), // Ajustez la largeur maximale souhaitée
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Column(
|
||||
children: [
|
||||
StyledTextField(
|
||||
labelText: 'Prénom',
|
||||
controller: _firstNameController),
|
||||
controller: firstNameController,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
StyledTextField(
|
||||
labelText: 'Nom',
|
||||
controller: _lastNameController),
|
||||
controller: lastNameController,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
StyledTextField(
|
||||
labelText: 'Numéro de téléphone',
|
||||
controller: _phoneController),
|
||||
controller: phoneController,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
StyledTextField(
|
||||
labelText: 'Email',
|
||||
controller:
|
||||
TextEditingController(text: user?.email ?? ''),
|
||||
TextEditingController(text: user.email),
|
||||
enabled: false,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _updateUserData,
|
||||
onPressed: () {
|
||||
userProvider.updateUserData(
|
||||
firstName: firstNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
phoneNumber: phoneController.text,
|
||||
);
|
||||
},
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
@ -235,6 +89,8 @@ class _MyAccountPageState extends State<MyAccountPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,66 +1,293 @@
|
||||
import 'package:em2rp/providers/local_auth_provider.dart';
|
||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../view_model/user_management_view_model.dart';
|
||||
import 'package:em2rp/providers/users_provider.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/views/widgets/user_management/user_card.dart';
|
||||
import 'package:em2rp/views/widgets/user_management/edit_user_dialog.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/utils/permission_gate.dart';
|
||||
import 'package:em2rp/models/role_model.dart';
|
||||
import 'package:em2rp/widgets/custom_app_bar.dart';
|
||||
|
||||
class UserManagementPage extends StatelessWidget {
|
||||
class UserManagementPage extends StatefulWidget {
|
||||
const UserManagementPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userViewModel = Provider.of<UserManagementViewModel>(context);
|
||||
final authProvider = Provider.of<LocalAuthProvider>(context);
|
||||
State<UserManagementPage> createState() => _UserManagementPageState();
|
||||
}
|
||||
|
||||
if (authProvider.role != 'ADMIN') {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Gestion des Utilisateurs')),
|
||||
body: const Center(
|
||||
child: Text('Accès non autorisé',
|
||||
style: TextStyle(color: Colors.red))),
|
||||
);
|
||||
class _UserManagementPageState extends State<UserManagementPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
Provider.of<UsersProvider>(context, listen: false).fetchUsers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Gestion des Utilisateurs')),
|
||||
body: userViewModel.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
itemCount: userViewModel.users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = userViewModel.users[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(user.profilePhotoUrl)),
|
||||
title: Text('${user.firstName} ${user.lastName}'),
|
||||
subtitle: Text(user.email),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
// Afficher la pop-up d'édition
|
||||
@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),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.lock_reset),
|
||||
onPressed: () =>
|
||||
userViewModel.resetPassword(user.email),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => userViewModel.deleteUser(user.uid),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
// Ajouter un utilisateur
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
width: 400,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.person_add, color: AppColors.rouge),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Nouvel utilisateur',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: firstNameController,
|
||||
decoration:
|
||||
buildInputDecoration('Prénom', Icons.person_outline),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: lastNameController,
|
||||
decoration: buildInputDecoration('Nom', Icons.person),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: emailController,
|
||||
decoration:
|
||||
buildInputDecoration('Email', Icons.email_outlined),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
decoration: buildInputDecoration(
|
||||
'Téléphone', Icons.phone_outlined),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedRole,
|
||||
decoration: buildInputDecoration(
|
||||
'Rôle', Icons.admin_panel_settings_outlined),
|
||||
items: Roles.values.map((Role role) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: role.name,
|
||||
child: Text(role.name),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
if (newValue != null) {
|
||||
selectedRole = newValue;
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
return const Text(
|
||||
'Bienvenue !',
|
||||
textAlign: TextAlign.center,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: AppColors.gris.withOpacity(0.1),
|
||||
color: AppColors.gris.withAlpha(26),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.zero,
|
||||
child: Image.asset(
|
||||
|
@ -56,7 +56,7 @@ class ProfilePictureWidget extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: radius * 0.8, // Ajuster la taille du loader
|
||||
height: radius * 0.8,
|
||||
child: CircularProgressIndicator(
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2), // Indicateur de chargement
|
||||
),
|
||||
);
|
||||
|
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/views/calendar_page.dart';
|
||||
import 'package:em2rp/views/my_account_page.dart';
|
||||
@ -6,15 +6,20 @@ import 'package:em2rp/views/user_management_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/utils/permission_gate.dart';
|
||||
import 'package:em2rp/models/role_model.dart';
|
||||
|
||||
class MainDrawer extends StatelessWidget {
|
||||
final String currentPage;
|
||||
|
||||
const MainDrawer({super.key, required this.currentPage});
|
||||
const MainDrawer({
|
||||
super.key,
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<LocalAuthProvider>(
|
||||
return Consumer<LocalUserProvider>(
|
||||
builder: (context, userProvider, child) {
|
||||
final hasUser = userProvider.currentUser != null;
|
||||
|
||||
@ -25,10 +30,10 @@ class MainDrawer extends StatelessWidget {
|
||||
DrawerHeader(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/EM2_NsurB.jpg'),
|
||||
image: const AssetImage('assets/EM2_NsurB.jpg'),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
AppColors.noir.withOpacity(0.4),
|
||||
AppColors.noir.withAlpha(102),
|
||||
BlendMode.darken,
|
||||
),
|
||||
),
|
||||
@ -47,7 +52,7 @@ class MainDrawer extends StatelessWidget {
|
||||
radius: 30,
|
||||
)
|
||||
else
|
||||
CircleAvatar(
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
child: Icon(Icons.account_circle, size: 45),
|
||||
),
|
||||
@ -56,7 +61,7 @@ class MainDrawer extends StatelessWidget {
|
||||
hasUser
|
||||
? 'Bonjour, ${userProvider.currentUser!.firstName}'
|
||||
: 'Bonjour, Utilisateur',
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: AppColors.blanc,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -83,7 +88,8 @@ class MainDrawer extends StatelessWidget {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => CalendarPage()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CalendarPage()),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -110,7 +116,9 @@ class MainDrawer extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
PermissionGate(
|
||||
requiredPermissions: const [Permission.viewUsers],
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.group),
|
||||
title: const Text('Gestion des Utilisateurs'),
|
||||
selected: currentPage == '/user_management',
|
||||
@ -120,10 +128,12 @@ class MainDrawer extends StatelessWidget {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const UserManagementPage()),
|
||||
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
|
||||
description: "A new Flutter project."
|
||||
publish_to: 'none'
|
||||
version: 0.1.0
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -9,6 +9,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
firebase_core: ^3.12.1
|
||||
firebase_auth: ^5.5.1
|
||||
cloud_firestore: ^5.6.5
|
||||
@ -17,6 +18,40 @@ dependencies:
|
||||
firebase_storage: ^12.4.4
|
||||
image_picker: ^1.1.2
|
||||
universal_io: ^2.2.2
|
||||
cupertino_icons: ^1.0.2
|
||||
table_calendar: ^3.0.9
|
||||
intl: ^0.19.0
|
||||
google_maps_flutter: ^2.5.0
|
||||
permission_handler: ^11.1.0
|
||||
geolocator: ^10.1.0
|
||||
flutter_map: ^6.1.0
|
||||
latlong2: ^0.9.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_native_splash: ^2.3.9
|
||||
url_launcher: ^6.2.2
|
||||
share_plus: ^7.2.1
|
||||
path_provider: ^2.1.2
|
||||
pdf: ^3.10.7
|
||||
printing: ^5.11.1
|
||||
flutter_local_notifications: ^16.3.0
|
||||
timezone: ^0.9.2
|
||||
flutter_secure_storage: ^9.0.0
|
||||
http: ^1.1.2
|
||||
flutter_dotenv: ^5.1.0
|
||||
google_fonts: ^6.1.0
|
||||
flutter_svg: ^2.0.9
|
||||
cached_network_image: ^3.3.1
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
shimmer: ^3.0.0
|
||||
flutter_slidable: ^3.0.1
|
||||
flutter_datetime_picker: ^1.5.1
|
||||
flutter_colorpicker: ^1.0.3
|
||||
flutter_rating_bar: ^4.0.1
|
||||
flutter_chat_ui: ^1.6.10
|
||||
flutter_chat_types: ^3.6.2
|
||||
uuid: ^4.2.2
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user