Compare commits
2 Commits
2c61b9ce8d
...
851b891a8a
Author | SHA1 | Date | |
---|---|---|---|
851b891a8a | |||
249a6d6074 |
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 'pages/auth/reset_password_page.dart';
|
||||
import 'config/env.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
void main() async {
|
||||
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(),
|
||||
routes: {
|
||||
'/login': (context) => const LoginPage(),
|
||||
|
@ -1,100 +1,28 @@
|
||||
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
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
// 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 {
|
||||
class RoleModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final Set<Permission> permissions;
|
||||
final List<String> permissions;
|
||||
|
||||
const Role({
|
||||
RoleModel({
|
||||
required this.id,
|
||||
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));
|
||||
factory RoleModel.fromMap(Map<String, dynamic> map, String id) {
|
||||
return RoleModel(
|
||||
id: id,
|
||||
name: map['name'] ?? '',
|
||||
permissions: List<String>.from(map['permissions'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasAnyPermission(List<Permission> requiredPermissions) {
|
||||
return requiredPermissions
|
||||
.any((permission) => permissions.contains(permission));
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'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 {
|
||||
final String uid;
|
||||
final String firstName;
|
||||
@ -19,11 +21,20 @@ class UserModel {
|
||||
|
||||
// Convertit une Map (Firestore) en UserModel
|
||||
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(
|
||||
uid: uid,
|
||||
firstName: data['firstName'] ?? '',
|
||||
lastName: data['lastName'] ?? '',
|
||||
role: data['role'] ?? 'USER',
|
||||
role: roleString,
|
||||
profilePhotoUrl: data['profilePhotoUrl'] ?? '',
|
||||
email: data['email'] ?? '',
|
||||
phoneNumber: data['phoneNumber'] ?? '',
|
||||
|
@ -11,24 +11,29 @@ class EventProvider with ChangeNotifier {
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
// 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;
|
||||
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(
|
||||
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
||||
QuerySnapshot eventsSnapshot;
|
||||
if (canViewAllEvents) {
|
||||
eventsSnapshot = await _firestore.collection('events').get();
|
||||
} else {
|
||||
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);
|
||||
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||
}).toList();
|
||||
|
||||
print('Parsed ${_events.length} events');
|
||||
|
@ -3,10 +3,12 @@ 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 '../models/role_model.dart';
|
||||
import '../utils/firebase_storage_manager.dart';
|
||||
|
||||
class LocalUserProvider with ChangeNotifier {
|
||||
UserModel? _currentUser;
|
||||
RoleModel? _currentRole;
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
|
||||
@ -19,6 +21,8 @@ class LocalUserProvider with ChangeNotifier {
|
||||
String? get profilePhotoUrl => _currentUser?.profilePhotoUrl;
|
||||
String? get email => _currentUser?.email;
|
||||
String? get phoneNumber => _currentUser?.phoneNumber;
|
||||
RoleModel? get currentRole => _currentRole;
|
||||
List<String> get permissions => _currentRole?.permissions ?? [];
|
||||
|
||||
/// Charge les données de l'utilisateur actuel
|
||||
Future<void> loadUserData() async {
|
||||
@ -47,6 +51,7 @@ class LocalUserProvider with ChangeNotifier {
|
||||
|
||||
setUser(UserModel.fromMap(userData, userDoc.id));
|
||||
print('User data loaded successfully');
|
||||
await loadRole();
|
||||
} else {
|
||||
print('No user document found in Firestore');
|
||||
// Créer un document utilisateur par défaut
|
||||
@ -73,6 +78,7 @@ class LocalUserProvider with ChangeNotifier {
|
||||
|
||||
setUser(defaultUser);
|
||||
print('Default user document created');
|
||||
await loadRole();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading user data: $e');
|
||||
@ -154,4 +160,24 @@ class LocalUserProvider with ChangeNotifier {
|
||||
await _auth.signOut();
|
||||
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:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
|
||||
class UsersProvider with ChangeNotifier {
|
||||
final UserService _userService;
|
||||
@ -75,7 +77,8 @@ class UsersProvider with ChangeNotifier {
|
||||
await _userService.resetPassword(email);
|
||||
}
|
||||
|
||||
Future<void> createUserWithEmailInvite(UserModel user) async {
|
||||
Future<void> createUserWithEmailInvite(
|
||||
BuildContext context, UserModel user) async {
|
||||
String? authUid;
|
||||
|
||||
try {
|
||||
@ -87,14 +90,12 @@ class UsersProvider with ChangeNotifier {
|
||||
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') {
|
||||
// Vérifier la permission via le provider
|
||||
final localUserProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
if (!localUserProvider.hasPermission('add_user')) {
|
||||
throw Exception(
|
||||
'Seuls les administrateurs peuvent créer des utilisateurs');
|
||||
'Vous n\'avez pas la permission de créer des utilisateurs');
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -5,7 +5,7 @@ import 'package:em2rp/providers/local_user_provider.dart';
|
||||
|
||||
class PermissionGate extends StatelessWidget {
|
||||
final Widget child;
|
||||
final List<Permission> requiredPermissions;
|
||||
final List<String> requiredPermissions;
|
||||
final bool requireAll;
|
||||
final Widget? fallback;
|
||||
|
||||
@ -26,10 +26,9 @@ class PermissionGate extends StatelessWidget {
|
||||
return fallback ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final userRole = Roles.fromString(currentUser.role);
|
||||
final hasPermission = requireAll
|
||||
? userRole.hasAllPermissions(requiredPermissions)
|
||||
: userRole.hasAnyPermission(requiredPermissions);
|
||||
? hasAllPermissions(localUserProvider, requiredPermissions)
|
||||
: hasAnyPermission(localUserProvider, requiredPermissions);
|
||||
|
||||
if (hasPermission) {
|
||||
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();
|
||||
|
||||
try {
|
||||
print('User signed in');
|
||||
|
||||
// Attendre que les données utilisateur soient chargées
|
||||
await localAuthProvider.loadUserData();
|
||||
|
||||
// --- É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
|
||||
await localAuthProvider.signInWithEmailAndPassword(
|
||||
emailController.text,
|
||||
passwordController.text,
|
||||
);
|
||||
// Vérifier si le contexte est toujours valide
|
||||
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) {
|
||||
// Utiliser pushReplacementNamed pour une transition propre
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pushReplacementNamed('/calendar');
|
||||
} else {
|
||||
errorMessage = 'Erreur lors du chargement des données utilisateur';
|
||||
errorMessage =
|
||||
'Erreur inattendue après connexion: Données utilisateur non chargées.';
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
} on FirebaseAuthException catch (e) {
|
||||
// Gestion spécifique des erreurs d'authentification (email/mot de passe incorrects, etc.)
|
||||
isLoading = false;
|
||||
errorMessage =
|
||||
e.message ?? 'Une erreur est survenue lors de la connexion';
|
||||
e.message ?? 'Une erreur est survenue lors de la connexion Firebase.';
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
// Gestion des autres erreurs potentielles (ex: erreur lors de loadUserData)
|
||||
isLoading = false;
|
||||
errorMessage = 'Une erreur inattendue est survenue';
|
||||
errorMessage = 'Une erreur inattendue est survenue: ${e.toString()}';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
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/custom_app_bar.dart';
|
||||
import 'package:em2rp/views/widgets/nav/main_drawer.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:em2rp/views/widgets/calendar_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';
|
||||
import 'package:em2rp/views/pages/event_add_page.dart';
|
||||
|
||||
class CalendarPage extends StatefulWidget {
|
||||
const CalendarPage({super.key});
|
||||
@ -36,9 +37,13 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final userId = localAuthProvider.uid;
|
||||
print('Permissions utilisateur: ${localAuthProvider.permissions}');
|
||||
final canViewAllEvents = localAuthProvider.hasPermission('view_all_events');
|
||||
print('canViewAllEvents: $canViewAllEvents');
|
||||
|
||||
if (userId != null) {
|
||||
await eventProvider.loadUserEvents(userId);
|
||||
await eventProvider.loadUserEvents(userId,
|
||||
canViewAllEvents: canViewAllEvents);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +56,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
@override
|
||||
Widget build(BuildContext 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;
|
||||
|
||||
if (eventProvider.isLoading) {
|
||||
@ -67,6 +74,20 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
),
|
||||
drawer: const MainDrawer(currentPage: '/calendar'),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -149,6 +170,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
onEventSelected: (event) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
_selectedDay = event.startDateTime;
|
||||
});
|
||||
},
|
||||
onSwitchToMonth: () {
|
||||
@ -156,6 +178,33 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
_calendarFormat = CalendarFormat.month;
|
||||
});
|
||||
},
|
||||
onDaySelected: (selectedDay) {
|
||||
final eventsForDay = eventProvider.events
|
||||
.where((e) =>
|
||||
e.startDateTime.year == selectedDay.year &&
|
||||
e.startDateTime.month == selectedDay.month &&
|
||||
e.startDateTime.day == selectedDay.day)
|
||||
.toList();
|
||||
eventsForDay
|
||||
.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
if (eventsForDay.isNotEmpty) {
|
||||
_selectedEvent = eventsForDay.first;
|
||||
} else {
|
||||
_selectedEvent = null;
|
||||
}
|
||||
});
|
||||
if (eventsForDay.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Aucun événement ne démarre à cette date."),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
selectedEvent: _selectedEvent,
|
||||
);
|
||||
} else {
|
||||
return MonthView(
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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';
|
||||
import 'package:em2rp/views/widgets/custom_app_bar.dart';
|
||||
|
||||
class MyAccountPage extends StatelessWidget {
|
||||
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/permission_gate.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 {
|
||||
const UserManagementPage({super.key});
|
||||
@ -31,7 +31,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PermissionGate(
|
||||
requiredPermissions: const [Permission.viewUsers],
|
||||
requiredPermissions: const ['view_all_users'],
|
||||
fallback: const Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Accès refusé',
|
||||
@ -110,7 +110,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
final lastNameController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
String selectedRole = Roles.values.first.name;
|
||||
String selectedRole = 'ADMIN';
|
||||
|
||||
InputDecoration buildInputDecoration(String label, IconData icon) {
|
||||
return InputDecoration(
|
||||
@ -188,10 +188,10 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
value: selectedRole,
|
||||
decoration: buildInputDecoration(
|
||||
'Rôle', Icons.admin_panel_settings_outlined),
|
||||
items: Roles.values.map((Role role) {
|
||||
items: ['ADMIN', 'CREW'].map((String role) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: role.name,
|
||||
child: Text(role.name),
|
||||
value: role,
|
||||
child: Text(role),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
@ -247,7 +247,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
||||
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
await Provider.of<UsersProvider>(context, listen: false)
|
||||
.createUserWithEmailInvite(newUser);
|
||||
.createUserWithEmailInvite(context, newUser);
|
||||
|
||||
if (context.mounted) {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ class WeekView extends StatelessWidget {
|
||||
final Function(int) onWeekChange;
|
||||
final Function(EventModel) onEventSelected;
|
||||
final Function() onSwitchToMonth;
|
||||
final Function(DateTime) onDaySelected;
|
||||
final EventModel? selectedEvent;
|
||||
|
||||
const WeekView({
|
||||
super.key,
|
||||
@ -17,6 +19,8 @@ class WeekView extends StatelessWidget {
|
||||
required this.onWeekChange,
|
||||
required this.onEventSelected,
|
||||
required this.onSwitchToMonth,
|
||||
required this.onDaySelected,
|
||||
required this.selectedEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -253,6 +257,8 @@ class WeekView extends StatelessWidget {
|
||||
final endHour = e.end.hour + e.end.minute / 60;
|
||||
final duration = endHour - startHour;
|
||||
final width = dayWidth / e.totalColumns;
|
||||
final isSelected =
|
||||
selectedEvent != null && e.event.id == selectedEvent!.id;
|
||||
|
||||
return Positioned(
|
||||
left: dayIdx * dayWidth + e.column * width,
|
||||
@ -265,8 +271,13 @@ class WeekView extends StatelessWidget {
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.rouge.withAlpha(26),
|
||||
border: Border.all(color: AppColors.rouge),
|
||||
color: isSelected
|
||||
? AppColors.rouge.withAlpha(80)
|
||||
: AppColors.rouge.withAlpha(26),
|
||||
border: Border.all(
|
||||
color: AppColors.rouge,
|
||||
width: isSelected ? 3 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
|
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(
|
||||
requiredPermissions: const [Permission.viewUsers],
|
||||
requiredPermissions: const ['view_all_users'],
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.group),
|
||||
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