Modifications des permissions, ajout Presta OK, vue calendrier ok
This commit is contained in:
9
.cursor/rules/rules1.mdc
Normal file
9
.cursor/rules/rules1.mdc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
description: To remember for every prompts
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
Le plus important : Repondre en français. Toujours appliquer les modification au code sauf si le message commence par "QUESTION :"
|
||||||
|
|
||||||
|
|
||||||
|
Projet d'ERP pour une entreprise d'événemetiel. Flutter, Dart
|
@ -15,6 +15,7 @@ import 'providers/local_user_provider.dart';
|
|||||||
import 'services/user_service.dart';
|
import 'services/user_service.dart';
|
||||||
import 'pages/auth/reset_password_page.dart';
|
import 'pages/auth/reset_password_page.dart';
|
||||||
import 'config/env.dart';
|
import 'config/env.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -81,6 +82,15 @@ class MyApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
locale: const Locale('fr', 'FR'),
|
||||||
|
supportedLocales: const [
|
||||||
|
Locale('fr', 'FR'),
|
||||||
|
],
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
home: const AutoLoginWrapper(),
|
home: const AutoLoginWrapper(),
|
||||||
routes: {
|
routes: {
|
||||||
'/login': (context) => const LoginPage(),
|
'/login': (context) => const LoginPage(),
|
||||||
|
@ -1,100 +1,28 @@
|
|||||||
enum Permission {
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
// 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
|
class RoleModel {
|
||||||
viewPrices, // Voir les prix
|
final String id;
|
||||||
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 String name;
|
||||||
final Set<Permission> permissions;
|
final List<String> permissions;
|
||||||
|
|
||||||
const Role({
|
RoleModel({
|
||||||
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.permissions,
|
required this.permissions,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool hasPermission(Permission permission) => permissions.contains(permission);
|
factory RoleModel.fromMap(Map<String, dynamic> map, String id) {
|
||||||
|
return RoleModel(
|
||||||
bool hasAllPermissions(List<Permission> requiredPermissions) {
|
id: id,
|
||||||
return requiredPermissions
|
name: map['name'] ?? '',
|
||||||
.every((permission) => permissions.contains(permission));
|
permissions: List<String>.from(map['permissions'] ?? []),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasAnyPermission(List<Permission> requiredPermissions) {
|
Map<String, dynamic> toMap() {
|
||||||
return requiredPermissions
|
return {
|
||||||
.any((permission) => permissions.contains(permission));
|
'name': name,
|
||||||
|
'permissions': permissions,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
class UserModel {
|
class UserModel {
|
||||||
final String uid;
|
final String uid;
|
||||||
final String firstName;
|
final String firstName;
|
||||||
@ -19,11 +21,20 @@ class UserModel {
|
|||||||
|
|
||||||
// Convertit une Map (Firestore) en UserModel
|
// Convertit une Map (Firestore) en UserModel
|
||||||
factory UserModel.fromMap(Map<String, dynamic> data, String uid) {
|
factory UserModel.fromMap(Map<String, dynamic> data, String uid) {
|
||||||
|
String roleString;
|
||||||
|
final roleField = data['role'];
|
||||||
|
if (roleField is String) {
|
||||||
|
roleString = roleField;
|
||||||
|
} else if (roleField is DocumentReference) {
|
||||||
|
roleString = roleField.id;
|
||||||
|
} else {
|
||||||
|
roleString = 'USER';
|
||||||
|
}
|
||||||
return UserModel(
|
return UserModel(
|
||||||
uid: uid,
|
uid: uid,
|
||||||
firstName: data['firstName'] ?? '',
|
firstName: data['firstName'] ?? '',
|
||||||
lastName: data['lastName'] ?? '',
|
lastName: data['lastName'] ?? '',
|
||||||
role: data['role'] ?? 'USER',
|
role: roleString,
|
||||||
profilePhotoUrl: data['profilePhotoUrl'] ?? '',
|
profilePhotoUrl: data['profilePhotoUrl'] ?? '',
|
||||||
email: data['email'] ?? '',
|
email: data['email'] ?? '',
|
||||||
phoneNumber: data['phoneNumber'] ?? '',
|
phoneNumber: data['phoneNumber'] ?? '',
|
||||||
|
@ -11,24 +11,29 @@ class EventProvider with ChangeNotifier {
|
|||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
|
|
||||||
// Récupérer les événements pour un utilisateur spécifique
|
// Récupérer les événements pour un utilisateur spécifique
|
||||||
Future<void> loadUserEvents(String userId) async {
|
Future<void> loadUserEvents(String userId,
|
||||||
|
{bool canViewAllEvents = false}) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('Loading events for user: $userId');
|
print(
|
||||||
|
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
||||||
// Récupérer uniquement les événements où l'utilisateur est dans la workforce
|
QuerySnapshot eventsSnapshot;
|
||||||
final eventsSnapshot = await _firestore
|
if (canViewAllEvents) {
|
||||||
.collection('events')
|
eventsSnapshot = await _firestore.collection('events').get();
|
||||||
.where('workforce', arrayContains: userId)
|
} else {
|
||||||
.get();
|
eventsSnapshot = await _firestore
|
||||||
|
.collection('events')
|
||||||
|
.where('workforce', arrayContains: userId)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
print('Found ${eventsSnapshot.docs.length} events for user');
|
print('Found ${eventsSnapshot.docs.length} events for user');
|
||||||
|
|
||||||
_events = eventsSnapshot.docs.map((doc) {
|
_events = eventsSnapshot.docs.map((doc) {
|
||||||
print('Event data: ${doc.data()}');
|
print('Event data: ${doc.data()}');
|
||||||
return EventModel.fromMap(doc.data(), doc.id);
|
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
print('Parsed ${_events.length} events');
|
print('Parsed ${_events.length} events');
|
||||||
|
@ -3,10 +3,12 @@ import 'package:firebase_auth/firebase_auth.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import '../models/user_model.dart';
|
import '../models/user_model.dart';
|
||||||
|
import '../models/role_model.dart';
|
||||||
import '../utils/firebase_storage_manager.dart';
|
import '../utils/firebase_storage_manager.dart';
|
||||||
|
|
||||||
class LocalUserProvider with ChangeNotifier {
|
class LocalUserProvider with ChangeNotifier {
|
||||||
UserModel? _currentUser;
|
UserModel? _currentUser;
|
||||||
|
RoleModel? _currentRole;
|
||||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
|
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
|
||||||
@ -19,6 +21,8 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
String? get profilePhotoUrl => _currentUser?.profilePhotoUrl;
|
String? get profilePhotoUrl => _currentUser?.profilePhotoUrl;
|
||||||
String? get email => _currentUser?.email;
|
String? get email => _currentUser?.email;
|
||||||
String? get phoneNumber => _currentUser?.phoneNumber;
|
String? get phoneNumber => _currentUser?.phoneNumber;
|
||||||
|
RoleModel? get currentRole => _currentRole;
|
||||||
|
List<String> get permissions => _currentRole?.permissions ?? [];
|
||||||
|
|
||||||
/// Charge les données de l'utilisateur actuel
|
/// Charge les données de l'utilisateur actuel
|
||||||
Future<void> loadUserData() async {
|
Future<void> loadUserData() async {
|
||||||
@ -47,6 +51,7 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
|
|
||||||
setUser(UserModel.fromMap(userData, userDoc.id));
|
setUser(UserModel.fromMap(userData, userDoc.id));
|
||||||
print('User data loaded successfully');
|
print('User data loaded successfully');
|
||||||
|
await loadRole();
|
||||||
} else {
|
} else {
|
||||||
print('No user document found in Firestore');
|
print('No user document found in Firestore');
|
||||||
// Créer un document utilisateur par défaut
|
// Créer un document utilisateur par défaut
|
||||||
@ -73,6 +78,7 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
|
|
||||||
setUser(defaultUser);
|
setUser(defaultUser);
|
||||||
print('Default user document created');
|
print('Default user document created');
|
||||||
|
await loadRole();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading user data: $e');
|
print('Error loading user data: $e');
|
||||||
@ -154,4 +160,24 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
await _auth.signOut();
|
await _auth.signOut();
|
||||||
clearUser();
|
clearUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadRole() async {
|
||||||
|
if (_currentUser == null) return;
|
||||||
|
final roleId = _currentUser!.role;
|
||||||
|
if (roleId.isEmpty) return;
|
||||||
|
try {
|
||||||
|
final doc = await _firestore.collection('roles').doc(roleId).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
_currentRole =
|
||||||
|
RoleModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading role: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasPermission(String permission) {
|
||||||
|
return _currentRole?.permissions.contains(permission) ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import '../services/user_service.dart';
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
|
||||||
class UsersProvider with ChangeNotifier {
|
class UsersProvider with ChangeNotifier {
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
@ -75,7 +77,8 @@ class UsersProvider with ChangeNotifier {
|
|||||||
await _userService.resetPassword(email);
|
await _userService.resetPassword(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createUserWithEmailInvite(UserModel user) async {
|
Future<void> createUserWithEmailInvite(
|
||||||
|
BuildContext context, UserModel user) async {
|
||||||
String? authUid;
|
String? authUid;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -87,14 +90,12 @@ class UsersProvider with ChangeNotifier {
|
|||||||
throw Exception('Aucun utilisateur connecté');
|
throw Exception('Aucun utilisateur connecté');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier le rôle de l'utilisateur actuel
|
// Vérifier la permission via le provider
|
||||||
final currentUserDoc =
|
final localUserProvider =
|
||||||
await _firestore.collection('users').doc(currentUser.uid).get();
|
Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
print('Current user role: ${currentUserDoc.data()?['role']}');
|
if (!localUserProvider.hasPermission('add_user')) {
|
||||||
|
|
||||||
if (currentUserDoc.data()?['role'] != 'ADMIN') {
|
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Seuls les administrateurs peuvent créer des utilisateurs');
|
'Vous n\'avez pas la permission de créer des utilisateurs');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -5,7 +5,7 @@ import 'package:em2rp/providers/local_user_provider.dart';
|
|||||||
|
|
||||||
class PermissionGate extends StatelessWidget {
|
class PermissionGate extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final List<Permission> requiredPermissions;
|
final List<String> requiredPermissions;
|
||||||
final bool requireAll;
|
final bool requireAll;
|
||||||
final Widget? fallback;
|
final Widget? fallback;
|
||||||
|
|
||||||
@ -26,10 +26,9 @@ class PermissionGate extends StatelessWidget {
|
|||||||
return fallback ?? const SizedBox.shrink();
|
return fallback ?? const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final userRole = Roles.fromString(currentUser.role);
|
|
||||||
final hasPermission = requireAll
|
final hasPermission = requireAll
|
||||||
? userRole.hasAllPermissions(requiredPermissions)
|
? hasAllPermissions(localUserProvider, requiredPermissions)
|
||||||
: userRole.hasAnyPermission(requiredPermissions);
|
: hasAnyPermission(localUserProvider, requiredPermissions);
|
||||||
|
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
return child;
|
return child;
|
||||||
@ -39,4 +38,18 @@ class PermissionGate extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasAllPermissions(LocalUserProvider provider, List<String> permissions) {
|
||||||
|
for (final perm in permissions) {
|
||||||
|
if (!provider.hasPermission(perm)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasAnyPermission(LocalUserProvider provider, List<String> permissions) {
|
||||||
|
for (final perm in permissions) {
|
||||||
|
if (provider.hasPermission(perm)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,32 +27,36 @@ class LoginViewModel extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('User signed in');
|
// --- Étape 1: Connecter l'utilisateur dans Firebase Auth ---
|
||||||
|
// Appelle la méthode du provider qui gère la connexion Auth ET le chargement des données utilisateur
|
||||||
// Attendre que les données utilisateur soient chargées
|
await localAuthProvider.signInWithEmailAndPassword(
|
||||||
await localAuthProvider.loadUserData();
|
emailController.text,
|
||||||
|
passwordController.text,
|
||||||
|
);
|
||||||
// Vérifier si le contexte est toujours valide
|
// Vérifier si le contexte est toujours valide
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
// Vérifier si l'utilisateur a bien été chargé
|
// Vérifier si l'utilisateur a bien été chargé dans le provider
|
||||||
if (localAuthProvider.currentUser != null) {
|
if (localAuthProvider.currentUser != null) {
|
||||||
// Utiliser pushReplacementNamed pour une transition propre
|
// Utiliser pushReplacementNamed pour une transition propre
|
||||||
Navigator.of(context, rootNavigator: true)
|
Navigator.of(context, rootNavigator: true)
|
||||||
.pushReplacementNamed('/calendar');
|
.pushReplacementNamed('/calendar');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = 'Erreur lors du chargement des données utilisateur';
|
errorMessage =
|
||||||
|
'Erreur inattendue après connexion: Données utilisateur non chargées.';
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on FirebaseAuthException catch (e) {
|
} on FirebaseAuthException catch (e) {
|
||||||
|
// Gestion spécifique des erreurs d'authentification (email/mot de passe incorrects, etc.)
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
errorMessage =
|
errorMessage =
|
||||||
e.message ?? 'Une erreur est survenue lors de la connexion';
|
e.message ?? 'Une erreur est survenue lors de la connexion Firebase.';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Gestion des autres erreurs potentielles (ex: erreur lors de loadUserData)
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
errorMessage = 'Une erreur inattendue est survenue';
|
errorMessage = 'Une erreur inattendue est survenue: ${e.toString()}';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:em2rp/widgets/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/custom_app_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/widgets/event_details.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details.dart';
|
||||||
import 'package:intl/date_symbol_data_local.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/month_view.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
|
||||||
|
import 'package:em2rp/views/pages/event_add_page.dart';
|
||||||
|
|
||||||
class CalendarPage extends StatefulWidget {
|
class CalendarPage extends StatefulWidget {
|
||||||
const CalendarPage({super.key});
|
const CalendarPage({super.key});
|
||||||
@ -36,9 +37,13 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
Provider.of<LocalUserProvider>(context, listen: false);
|
Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
final userId = localAuthProvider.uid;
|
final userId = localAuthProvider.uid;
|
||||||
|
print('Permissions utilisateur: ${localAuthProvider.permissions}');
|
||||||
|
final canViewAllEvents = localAuthProvider.hasPermission('view_all_events');
|
||||||
|
print('canViewAllEvents: $canViewAllEvents');
|
||||||
|
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
await eventProvider.loadUserEvents(userId);
|
await eventProvider.loadUserEvents(userId,
|
||||||
|
canViewAllEvents: canViewAllEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +56,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final eventProvider = Provider.of<EventProvider>(context);
|
final eventProvider = Provider.of<EventProvider>(context);
|
||||||
|
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||||
|
final isAdmin = localUserProvider.role == 'ADMIN';
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
|
|
||||||
if (eventProvider.isLoading) {
|
if (eventProvider.isLoading) {
|
||||||
@ -67,6 +74,20 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
),
|
),
|
||||||
drawer: const MainDrawer(currentPage: '/calendar'),
|
drawer: const MainDrawer(currentPage: '/calendar'),
|
||||||
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
||||||
|
floatingActionButton: isAdmin
|
||||||
|
? FloatingActionButton(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const EventAddPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add, color: Colors.red),
|
||||||
|
tooltip: 'Ajouter un événement',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
|
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
|
||||||
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
|
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
|
||||||
import 'package:em2rp/widgets/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/custom_app_bar.dart';
|
||||||
|
|
||||||
class MyAccountPage extends StatelessWidget {
|
class MyAccountPage extends StatelessWidget {
|
||||||
const MyAccountPage({super.key});
|
const MyAccountPage({super.key});
|
||||||
|
410
em2rp/lib/views/pages/event_add_page.dart
Normal file
410
em2rp/lib/views/pages/event_add_page.dart
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart';
|
||||||
|
|
||||||
|
class EventAddPage extends StatefulWidget {
|
||||||
|
const EventAddPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EventAddPage> createState() => _EventAddPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EventAddPageState extends State<EventAddPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _descriptionController = TextEditingController();
|
||||||
|
final TextEditingController _priceController = TextEditingController();
|
||||||
|
final TextEditingController _installationController = TextEditingController();
|
||||||
|
final TextEditingController _disassemblyController = TextEditingController();
|
||||||
|
final TextEditingController _latitudeController = TextEditingController();
|
||||||
|
final TextEditingController _longitudeController = TextEditingController();
|
||||||
|
DateTime? _startDateTime;
|
||||||
|
DateTime? _endDateTime;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String? _success;
|
||||||
|
String? _selectedEventType;
|
||||||
|
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
||||||
|
int _descriptionMaxLines = 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_descriptionController.addListener(_handleDescriptionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDescriptionChange() {
|
||||||
|
final lines = '\n'.allMatches(_descriptionController.text).length + 1;
|
||||||
|
setState(() {
|
||||||
|
_descriptionMaxLines = lines.clamp(3, 6);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
_priceController.dispose();
|
||||||
|
_installationController.dispose();
|
||||||
|
_disassemblyController.dispose();
|
||||||
|
_latitudeController.dispose();
|
||||||
|
_longitudeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (!_formKey.currentState!.validate() ||
|
||||||
|
_startDateTime == null ||
|
||||||
|
_endDateTime == null ||
|
||||||
|
_selectedEventType == null) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
_success = null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
|
final newEvent = EventModel(
|
||||||
|
id: '',
|
||||||
|
name: _nameController.text.trim(),
|
||||||
|
description: _descriptionController.text.trim(),
|
||||||
|
startDateTime: _startDateTime!,
|
||||||
|
endDateTime: _endDateTime!,
|
||||||
|
price: double.tryParse(_priceController.text) ?? 0.0,
|
||||||
|
installationTime: int.tryParse(_installationController.text) ?? 0,
|
||||||
|
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
|
||||||
|
eventTypeId: _selectedEventType!,
|
||||||
|
customerId: '', // à adapter si tu veux gérer les clients
|
||||||
|
address: LatLng(
|
||||||
|
double.tryParse(_latitudeController.text) ?? 0.0,
|
||||||
|
double.tryParse(_longitudeController.text) ?? 0.0,
|
||||||
|
),
|
||||||
|
workforce: [],
|
||||||
|
);
|
||||||
|
await eventProvider.addEvent(newEvent);
|
||||||
|
setState(() {
|
||||||
|
_success = "Événement créé avec succès !";
|
||||||
|
});
|
||||||
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = "Erreur lors de la création : $e";
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionTitle(String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Créer un événement'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Card(
|
||||||
|
elevation: 6,
|
||||||
|
margin: const EdgeInsets.all(24),
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 32),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 0.0, bottom: 4.0),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Informations principales',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nom de l\'événement',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.event),
|
||||||
|
),
|
||||||
|
validator: (v) =>
|
||||||
|
v == null || v.isEmpty ? 'Champ requis' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedEventType,
|
||||||
|
items: _eventTypes
|
||||||
|
.map((type) => DropdownMenuItem<String>(
|
||||||
|
value: type,
|
||||||
|
child: Text(type),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (val) =>
|
||||||
|
setState(() => _selectedEventType = val),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Type d\'événement',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.category),
|
||||||
|
),
|
||||||
|
validator: (v) =>
|
||||||
|
v == null ? 'Sélectionnez un type' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2099),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
final time = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (time != null) {
|
||||||
|
setState(() {
|
||||||
|
_startDateTime = DateTime(
|
||||||
|
picked.year,
|
||||||
|
picked.month,
|
||||||
|
picked.day,
|
||||||
|
time.hour,
|
||||||
|
time.minute,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Début',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.calendar_today),
|
||||||
|
suffixIcon: const Icon(Icons.edit_calendar),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: _startDateTime == null
|
||||||
|
? ''
|
||||||
|
: DateFormat('dd/MM/yyyy HH:mm')
|
||||||
|
.format(_startDateTime!),
|
||||||
|
),
|
||||||
|
validator: (v) => _startDateTime == null
|
||||||
|
? 'Champ requis'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _startDateTime ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2099),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
final time = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (time != null) {
|
||||||
|
setState(() {
|
||||||
|
_endDateTime = DateTime(
|
||||||
|
picked.year,
|
||||||
|
picked.month,
|
||||||
|
picked.day,
|
||||||
|
time.hour,
|
||||||
|
time.minute,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Fin',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.calendar_today),
|
||||||
|
suffixIcon: const Icon(Icons.edit_calendar),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: _endDateTime == null
|
||||||
|
? ''
|
||||||
|
: DateFormat('dd/MM/yyyy HH:mm')
|
||||||
|
.format(_endDateTime!),
|
||||||
|
),
|
||||||
|
validator: (v) => _endDateTime == null
|
||||||
|
? 'Champ requis'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _priceController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Prix (€)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.euro),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
_buildSectionTitle('Détails'),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: 48,
|
||||||
|
maxHeight: 48.0 * 10,
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: _descriptionMaxLines > 10
|
||||||
|
? 10
|
||||||
|
: _descriptionMaxLines,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.description),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: IntStepperField(
|
||||||
|
label: 'Installation (h)',
|
||||||
|
controller: _installationController,
|
||||||
|
min: 0,
|
||||||
|
max: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: IntStepperField(
|
||||||
|
label: 'Démontage (h)',
|
||||||
|
controller: _disassemblyController,
|
||||||
|
min: 0,
|
||||||
|
max: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_buildSectionTitle('Localisation'),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _latitudeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Latitude',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.location_on),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _longitudeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Longitude',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.location_on),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_error != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(_error!,
|
||||||
|
style: const TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
if (_success != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(_success!,
|
||||||
|
style: const TextStyle(color: Colors.green)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
onPressed: _isLoading ? null : _submit,
|
||||||
|
label: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Créer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ import 'package:em2rp/views/widgets/user_management/edit_user_dialog.dart';
|
|||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
import 'package:em2rp/utils/permission_gate.dart';
|
import 'package:em2rp/utils/permission_gate.dart';
|
||||||
import 'package:em2rp/models/role_model.dart';
|
import 'package:em2rp/models/role_model.dart';
|
||||||
import 'package:em2rp/widgets/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/custom_app_bar.dart';
|
||||||
|
|
||||||
class UserManagementPage extends StatefulWidget {
|
class UserManagementPage extends StatefulWidget {
|
||||||
const UserManagementPage({super.key});
|
const UserManagementPage({super.key});
|
||||||
@ -31,7 +31,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PermissionGate(
|
return PermissionGate(
|
||||||
requiredPermissions: const [Permission.viewUsers],
|
requiredPermissions: const ['view_all_users'],
|
||||||
fallback: const Scaffold(
|
fallback: const Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: 'Accès refusé',
|
title: 'Accès refusé',
|
||||||
@ -110,7 +110,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
final lastNameController = TextEditingController();
|
final lastNameController = TextEditingController();
|
||||||
final emailController = TextEditingController();
|
final emailController = TextEditingController();
|
||||||
final phoneController = TextEditingController();
|
final phoneController = TextEditingController();
|
||||||
String selectedRole = Roles.values.first.name;
|
String selectedRole = 'ADMIN';
|
||||||
|
|
||||||
InputDecoration buildInputDecoration(String label, IconData icon) {
|
InputDecoration buildInputDecoration(String label, IconData icon) {
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
@ -188,10 +188,10 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
value: selectedRole,
|
value: selectedRole,
|
||||||
decoration: buildInputDecoration(
|
decoration: buildInputDecoration(
|
||||||
'Rôle', Icons.admin_panel_settings_outlined),
|
'Rôle', Icons.admin_panel_settings_outlined),
|
||||||
items: Roles.values.map((Role role) {
|
items: ['ADMIN', 'CREW'].map((String role) {
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
value: role.name,
|
value: role,
|
||||||
child: Text(role.name),
|
child: Text(role),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (String? newValue) {
|
onChanged: (String? newValue) {
|
||||||
@ -247,7 +247,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
|
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
await Provider.of<UsersProvider>(context, listen: false)
|
await Provider.of<UsersProvider>(context, listen: false)
|
||||||
.createUserWithEmailInvite(newUser);
|
.createUserWithEmailInvite(context, newUser);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
420
em2rp/lib/views/widgets/calendar_widgets/event_details.dart
Normal file
420
em2rp/lib/views/widgets/calendar_widgets/event_details.dart
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
|
import 'package:latlong2/latlong.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);
|
||||||
|
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||||
|
final isAdmin = localUserProvider.role == 'ADMIN';
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventAddDialog extends StatefulWidget {
|
||||||
|
const EventAddDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EventAddDialog> createState() => _EventAddDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EventAddDialogState extends State<EventAddDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _descriptionController = TextEditingController();
|
||||||
|
final TextEditingController _priceController = TextEditingController();
|
||||||
|
final TextEditingController _installationController = TextEditingController();
|
||||||
|
final TextEditingController _disassemblyController = TextEditingController();
|
||||||
|
final TextEditingController _latitudeController = TextEditingController();
|
||||||
|
final TextEditingController _longitudeController = TextEditingController();
|
||||||
|
DateTime? _startDateTime;
|
||||||
|
DateTime? _endDateTime;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String? _success;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
_priceController.dispose();
|
||||||
|
_installationController.dispose();
|
||||||
|
_disassemblyController.dispose();
|
||||||
|
_latitudeController.dispose();
|
||||||
|
_longitudeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (!_formKey.currentState!.validate() ||
|
||||||
|
_startDateTime == null ||
|
||||||
|
_endDateTime == null) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
_success = null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
|
final newEvent = EventModel(
|
||||||
|
id: '',
|
||||||
|
name: _nameController.text.trim(),
|
||||||
|
description: _descriptionController.text.trim(),
|
||||||
|
startDateTime: _startDateTime!,
|
||||||
|
endDateTime: _endDateTime!,
|
||||||
|
price: double.tryParse(_priceController.text) ?? 0.0,
|
||||||
|
installationTime: int.tryParse(_installationController.text) ?? 0,
|
||||||
|
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
|
||||||
|
eventTypeId: '', // à adapter si tu veux gérer les types
|
||||||
|
customerId: '', // à adapter si tu veux gérer les clients
|
||||||
|
address: LatLng(
|
||||||
|
double.tryParse(_latitudeController.text) ?? 0.0,
|
||||||
|
double.tryParse(_longitudeController.text) ?? 0.0,
|
||||||
|
),
|
||||||
|
workforce: [],
|
||||||
|
);
|
||||||
|
await eventProvider.addEvent(newEvent);
|
||||||
|
setState(() {
|
||||||
|
_success = "Événement créé avec succès !";
|
||||||
|
});
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = "Erreur lors de la création : $e";
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Créer un événement'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Nom'),
|
||||||
|
validator: (v) =>
|
||||||
|
v == null || v.isEmpty ? 'Champ requis' : null,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Description'),
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _priceController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Prix (€)'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _installationController,
|
||||||
|
decoration:
|
||||||
|
const InputDecoration(labelText: 'Installation (h)'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _disassemblyController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Démontage (h)'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _latitudeController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Latitude'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _longitudeController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Longitude'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2030),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
final time = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (time != null) {
|
||||||
|
setState(() {
|
||||||
|
_startDateTime = DateTime(
|
||||||
|
picked.year,
|
||||||
|
picked.month,
|
||||||
|
picked.day,
|
||||||
|
time.hour,
|
||||||
|
time.minute,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(_startDateTime == null
|
||||||
|
? 'Début'
|
||||||
|
: DateFormat('dd/MM/yyyy HH:mm')
|
||||||
|
.format(_startDateTime!)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _startDateTime ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2030),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
final time = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (time != null) {
|
||||||
|
setState(() {
|
||||||
|
_endDateTime = DateTime(
|
||||||
|
picked.year,
|
||||||
|
picked.month,
|
||||||
|
picked.day,
|
||||||
|
time.hour,
|
||||||
|
time.minute,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(_endDateTime == null
|
||||||
|
? 'Fin'
|
||||||
|
: DateFormat('dd/MM/yyyy HH:mm')
|
||||||
|
.format(_endDateTime!)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_error != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child:
|
||||||
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
if (_success != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(_success!,
|
||||||
|
style: const TextStyle(color: Colors.green)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _submit,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Créer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
87
em2rp/lib/views/widgets/inputs/int_stepper_field.dart
Normal file
87
em2rp/lib/views/widgets/inputs/int_stepper_field.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class IntStepperField extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
|
||||||
|
const IntStepperField({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.controller,
|
||||||
|
this.min = 0,
|
||||||
|
this.max = 24,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
int value = int.tryParse(controller.text) ?? 0;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2.0),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove_circle_outline, size: 20),
|
||||||
|
splashRadius: 18,
|
||||||
|
onPressed: value > min
|
||||||
|
? () {
|
||||||
|
value--;
|
||||||
|
controller.text = value.toString();
|
||||||
|
(context as Element).markNeedsBuild();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 60,
|
||||||
|
height: 36,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||||
|
),
|
||||||
|
onChanged: (val) {
|
||||||
|
int? v = int.tryParse(val);
|
||||||
|
if (v == null || v < min) {
|
||||||
|
controller.text = min.toString();
|
||||||
|
} else if (v > max) {
|
||||||
|
controller.text = max.toString();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add_circle_outline, size: 20),
|
||||||
|
splashRadius: 18,
|
||||||
|
onPressed: value < max
|
||||||
|
? () {
|
||||||
|
value++;
|
||||||
|
controller.text = value.toString();
|
||||||
|
(context as Element).markNeedsBuild();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -117,7 +117,7 @@ class MainDrawer extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
PermissionGate(
|
PermissionGate(
|
||||||
requiredPermissions: const [Permission.viewUsers],
|
requiredPermissions: const ['view_all_users'],
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.group),
|
leading: const Icon(Icons.group),
|
||||||
title: const Text('Gestion des Utilisateurs'),
|
title: const Text('Gestion des Utilisateurs'),
|
||||||
|
@ -1,172 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user