diff --git a/em2rp/firebase.json b/em2rp/firebase.json index 7e8df10..856bc09 100644 --- a/em2rp/firebase.json +++ b/em2rp/firebase.json @@ -34,5 +34,13 @@ "*.local" ] } - ] + ], + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } } diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index 5119ec2..ea3305c 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -9,6 +9,11 @@ const {onRequest} = require("firebase-functions/v2/https"); const logger = require("firebase-functions/logger"); +const admin = require('firebase-admin'); +const { Storage } = require('@google-cloud/storage'); + +admin.initializeApp(); +const storage = new Storage(); // Create and deploy your first functions // https://firebase.google.com/docs/functions/get-started @@ -18,22 +23,12 @@ const logger = require("firebase-functions/logger"); // response.send("Hello from Firebase!"); // }); -const functions = require('firebase-functions'); -const admin = require('firebase-admin'); -const { Storage } = require('@google-cloud/storage'); -admin.initializeApp(); -const storage = new Storage(); // Nouvelle version HTTP sécurisée -exports.moveEventFileV2 = functions.https.onRequest(async (req, res) => { - // Ajout des headers CORS - res.set('Access-Control-Allow-Origin', '*'); - res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - if (req.method === 'OPTIONS') { - res.set('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.status(204).send(''); - return; - } +exports.moveEventFileV2 = onRequest({cors: true}, async (req, res) => { + // La gestion CORS est maintenant gérée par l'option {cors: true} + // La vérification pour les requêtes OPTIONS n'est plus nécessaire + // Vérification du token Firebase dans l'en-tête Authorization let uid = null; if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { @@ -42,11 +37,13 @@ exports.moveEventFileV2 = functions.https.onRequest(async (req, res) => { const decodedToken = await admin.auth().verifyIdToken(idToken); uid = decodedToken.uid; } catch (e) { - res.status(401).json({ error: 'Invalid token' }); + logger.error("Error while verifying Firebase ID token:", e); + res.status(401).json({ error: 'Unauthorized: Invalid token' }); return; } } else { - res.status(401).json({ error: 'No token provided' }); + logger.warn("No Firebase ID token was passed as a Bearer token in the Authorization header."); + res.status(401).json({ error: 'Unauthorized: No token provided' }); return; } @@ -68,6 +65,7 @@ exports.moveEventFileV2 = functions.https.onRequest(async (req, res) => { }); res.status(200).json({ url }); } catch (error) { + logger.error("Error moving file:", error); res.status(500).json({ error: error.message }); } }); \ No newline at end of file diff --git a/em2rp/lib/config/env.dart b/em2rp/lib/config/env.dart index df62d3d..6d88247 100644 --- a/em2rp/lib/config/env.dart +++ b/em2rp/lib/config/env.dart @@ -1,5 +1,5 @@ class Env { - static const bool isDevelopment = true; + static const bool isDevelopment = false; // Configuration de l'auto-login en développement static const String devAdminEmail = 'paul.fournel@em2events.fr'; diff --git a/em2rp/lib/controllers/event_form_controller.dart b/em2rp/lib/controllers/event_form_controller.dart new file mode 100644 index 0000000..951fe56 --- /dev/null +++ b/em2rp/lib/controllers/event_form_controller.dart @@ -0,0 +1,386 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/models/event_type_model.dart'; +import 'package:em2rp/models/user_model.dart'; +import 'package:em2rp/services/event_form_service.dart'; +import 'package:provider/provider.dart'; +import 'package:em2rp/providers/event_provider.dart'; +import 'package:em2rp/providers/local_user_provider.dart'; + +class EventFormController extends ChangeNotifier { + // Controllers + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController basePriceController = TextEditingController(); + final TextEditingController installationController = TextEditingController(); + final TextEditingController disassemblyController = TextEditingController(); + final TextEditingController addressController = TextEditingController(); + + // State variables + DateTime? _startDateTime; + DateTime? _endDateTime; + bool _isLoading = false; + String? _error; + String? _success; + String? _selectedEventTypeId; + List _eventTypes = []; + bool _isLoadingEventTypes = true; + List _selectedUserIds = []; + List _allUsers = []; + bool _isLoadingUsers = true; + List> _uploadedFiles = []; + List> _selectedOptions = []; + bool _formChanged = false; + EventStatus _selectedStatus = EventStatus.waitingForApproval; + + // Getters + DateTime? get startDateTime => _startDateTime; + DateTime? get endDateTime => _endDateTime; + bool get isLoading => _isLoading; + String? get error => _error; + String? get success => _success; + String? get selectedEventTypeId => _selectedEventTypeId; + List get eventTypes => _eventTypes; + bool get isLoadingEventTypes => _isLoadingEventTypes; + List get selectedUserIds => _selectedUserIds; + List get allUsers => _allUsers; + bool get isLoadingUsers => _isLoadingUsers; + List> get uploadedFiles => _uploadedFiles; + List> get selectedOptions => _selectedOptions; + bool get formChanged => _formChanged; + EventStatus get selectedStatus => _selectedStatus; + + EventFormController() { + _setupListeners(); + } + + void _setupListeners() { + nameController.addListener(_onAnyFieldChanged); + basePriceController.addListener(_onAnyFieldChanged); + installationController.addListener(_onAnyFieldChanged); + disassemblyController.addListener(_onAnyFieldChanged); + addressController.addListener(_onAnyFieldChanged); + descriptionController.addListener(_onAnyFieldChanged); + } + + void _onAnyFieldChanged() { + if (!_formChanged) { + _formChanged = true; + notifyListeners(); + } + } + + Future initialize([EventModel? existingEvent]) async { + await Future.wait([ + _fetchUsers(), + _fetchEventTypes(), + ]); + + if (existingEvent != null) { + _populateFromEvent(existingEvent); + } else { + _selectedStatus = EventStatus.waitingForApproval; + } + notifyListeners(); + } + + void _populateFromEvent(EventModel event) { + nameController.text = event.name; + descriptionController.text = event.description; + basePriceController.text = event.basePrice.toStringAsFixed(2); + installationController.text = event.installationTime.toString(); + disassemblyController.text = event.disassemblyTime.toString(); + addressController.text = event.address; + _startDateTime = event.startDateTime; + _endDateTime = event.endDateTime; + _selectedEventTypeId = event.eventTypeId.isNotEmpty ? event.eventTypeId : null; + _selectedUserIds = event.workforce.map((ref) => ref.id).toList(); + _uploadedFiles = List>.from(event.documents); + _selectedOptions = List>.from(event.options); + _selectedStatus = event.status; + } + + Future _fetchUsers() async { + try { + _allUsers = await EventFormService.fetchUsers(); + _isLoadingUsers = false; + } catch (e) { + _error = e.toString(); + _isLoadingUsers = false; + } + notifyListeners(); + } + + Future _fetchEventTypes() async { + try { + _eventTypes = await EventFormService.fetchEventTypes(); + _isLoadingEventTypes = false; + } catch (e) { + _error = e.toString(); + _isLoadingEventTypes = false; + } + notifyListeners(); + } + + void setStartDateTime(DateTime? dateTime) { + _startDateTime = dateTime; + if (_endDateTime != null && + dateTime != null && + (_endDateTime!.isBefore(dateTime) || _endDateTime!.isAtSameMomentAs(dateTime))) { + _endDateTime = null; + } + _onAnyFieldChanged(); + notifyListeners(); + } + + void setEndDateTime(DateTime? dateTime) { + _endDateTime = dateTime; + _onAnyFieldChanged(); + notifyListeners(); + } + + void onEventTypeChanged(String? newTypeId, BuildContext context) { + if (newTypeId == _selectedEventTypeId) return; + + final oldEventTypeIndex = _selectedEventTypeId != null + ? _eventTypes.indexWhere((et) => et.id == _selectedEventTypeId) + : -1; + final EventType? oldEventType = oldEventTypeIndex != -1 ? _eventTypes[oldEventTypeIndex] : null; + + _selectedEventTypeId = newTypeId; + + if (newTypeId != null) { + final selectedType = _eventTypes.firstWhere((et) => et.id == newTypeId); + final defaultPrice = selectedType.defaultPrice; + final currentPrice = double.tryParse(basePriceController.text.replaceAll(',', '.')); + final oldDefaultPrice = oldEventType?.defaultPrice; + + if (basePriceController.text.isEmpty || + (currentPrice != null && oldDefaultPrice != null && currentPrice == oldDefaultPrice)) { + basePriceController.text = defaultPrice.toStringAsFixed(2); + } + + final before = _selectedOptions.length; + _selectedOptions.removeWhere((opt) { + final types = opt['compatibleTypes'] as List?; + if (types == null) return true; + return !types.contains(selectedType.name); + }); + + if (_selectedOptions.length < before) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Certaines options ont été retirées car non compatibles avec "${selectedType.name}".')), + ); + } + } else { + _selectedOptions.clear(); + } + + _onAnyFieldChanged(); + notifyListeners(); + } + + void setSelectedUserIds(List userIds) { + _selectedUserIds = userIds; + _onAnyFieldChanged(); + notifyListeners(); + } + + void setUploadedFiles(List> files) { + _uploadedFiles = files; + _onAnyFieldChanged(); + notifyListeners(); + } + + void setSelectedOptions(List> options) { + _selectedOptions = options; + _onAnyFieldChanged(); + notifyListeners(); + } + + Future pickAndUploadFiles() async { + final result = await FilePicker.platform.pickFiles(allowMultiple: true, withData: true); + if (result != null && result.files.isNotEmpty) { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final files = await EventFormService.uploadFiles(result.files); + _uploadedFiles.addAll(files); + _onAnyFieldChanged(); + } catch (e) { + _error = 'Erreur lors de l\'upload : $e'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + } + + bool validateForm() { + return nameController.text.isNotEmpty && + _startDateTime != null && + _endDateTime != null && + _selectedEventTypeId != null && + addressController.text.isNotEmpty && + (_endDateTime!.isAfter(_startDateTime!)); + } + + Future submitForm(BuildContext context, {EventModel? existingEvent}) async { + if (!validateForm()) { + _error = "Veuillez remplir tous les champs obligatoires."; + notifyListeners(); + return false; + } + + _isLoading = true; + _error = null; + _success = null; + notifyListeners(); + + try { + final eventTypeRef = _selectedEventTypeId != null + ? FirebaseFirestore.instance.collection('eventTypes').doc(_selectedEventTypeId) + : null; + + if (existingEvent != null) { + // Mode édition + // Gérer les nouveaux fichiers uploadés s'il y en a + List> finalDocuments = List>.from(_uploadedFiles); + + // Identifier les nouveaux fichiers (ceux qui ont une URL temp) + final newFiles = _uploadedFiles.where((file) => + file['url']?.contains('events/temp/') ?? false).toList(); + + if (newFiles.isNotEmpty) { + // Déplacer les nouveaux fichiers vers le dossier de l'événement + final movedFiles = await EventFormService.moveFilesToEvent(newFiles, existingEvent.id); + + // Remplacer les URLs temporaires par les nouvelles URLs + for (int i = 0; i < finalDocuments.length; i++) { + final tempFile = finalDocuments[i]; + final movedFile = movedFiles.firstWhere( + (moved) => moved['name'] == tempFile['name'], + orElse: () => tempFile, + ); + finalDocuments[i] = movedFile; + } + } + + final updatedEvent = EventModel( + id: existingEvent.id, + name: nameController.text.trim(), + description: descriptionController.text.trim(), + startDateTime: _startDateTime!, + endDateTime: _endDateTime!, + basePrice: double.tryParse(basePriceController.text.replaceAll(',', '.')) ?? 0.0, + installationTime: int.tryParse(installationController.text) ?? 0, + disassemblyTime: int.tryParse(disassemblyController.text) ?? 0, + eventTypeId: _selectedEventTypeId!, + eventTypeRef: eventTypeRef, + customerId: existingEvent.customerId, + address: addressController.text.trim(), + workforce: _selectedUserIds + .map((id) => FirebaseFirestore.instance.collection('users').doc(id)) + .toList(), + latitude: existingEvent.latitude, + longitude: existingEvent.longitude, + documents: finalDocuments, + options: _selectedOptions, + status: _selectedStatus, + ); + + await EventFormService.updateEvent(updatedEvent); + + // Recharger les événements après modification + final localUserProvider = Provider.of(context, listen: false); + final eventProvider = Provider.of(context, listen: false); + final userId = localUserProvider.uid; + final canViewAllEvents = localUserProvider.hasPermission('view_all_events'); + + if (userId != null) { + await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents); + } + + _success = "Événement modifié avec succès !"; + } else { + // Mode création + final newEvent = EventModel( + id: '', + name: nameController.text.trim(), + description: descriptionController.text.trim(), + startDateTime: _startDateTime!, + endDateTime: _endDateTime!, + basePrice: double.tryParse(basePriceController.text.replaceAll(',', '.')) ?? 0.0, + installationTime: int.tryParse(installationController.text) ?? 0, + disassemblyTime: int.tryParse(disassemblyController.text) ?? 0, + eventTypeId: _selectedEventTypeId!, + eventTypeRef: eventTypeRef, + customerId: '', + address: addressController.text.trim(), + workforce: _selectedUserIds + .map((id) => FirebaseFirestore.instance.collection('users').doc(id)) + .toList(), + latitude: 0.0, + longitude: 0.0, + documents: _uploadedFiles, + options: _selectedOptions, + status: _selectedStatus, + ); + + final eventId = await EventFormService.createEvent(newEvent); + final newFiles = await EventFormService.moveFilesToEvent(_uploadedFiles, eventId); + await EventFormService.updateEventDocuments(eventId, newFiles); + + // Reload events + final localUserProvider = Provider.of(context, listen: false); + final eventProvider = Provider.of(context, listen: false); + final userId = localUserProvider.uid; + final canViewAllEvents = localUserProvider.hasPermission('view_all_events'); + + if (userId != null) { + await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents); + } + + _success = "Événement créé avec succès !"; + } + + _formChanged = false; + notifyListeners(); + return true; + } catch (e) { + _error = "Erreur lors de la sauvegarde : $e"; + notifyListeners(); + return false; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + void clearError() { + _error = null; + notifyListeners(); + } + + void clearSuccess() { + _success = null; + notifyListeners(); + } + + @override + void dispose() { + nameController.dispose(); + descriptionController.dispose(); + basePriceController.dispose(); + installationController.dispose(); + disassemblyController.dispose(); + addressController.dispose(); + super.dispose(); + } +} diff --git a/em2rp/lib/models/event_model.dart b/em2rp/lib/models/event_model.dart index afb5a3d..c521d12 100644 --- a/em2rp/lib/models/event_model.dart +++ b/em2rp/lib/models/event_model.dart @@ -1,5 +1,4 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:latlong2/latlong.dart'; enum EventStatus { confirmed, @@ -41,6 +40,7 @@ class EventModel { final int installationTime; final int disassemblyTime; final String eventTypeId; + final DocumentReference? eventTypeRef; final String customerId; final String address; final double latitude; @@ -60,6 +60,7 @@ class EventModel { required this.installationTime, required this.disassemblyTime, required this.eventTypeId, + this.eventTypeRef, required this.customerId, required this.address, required this.latitude, @@ -79,7 +80,7 @@ class EventModel { final docs = docsRaw is List ? docsRaw.map>((e) { if (e is Map) { - return Map.from(e as Map); + return Map.from(e); } else if (e is String) { final fileName = Uri.decodeComponent( e.split('/').last.split('?').first, @@ -94,7 +95,7 @@ class EventModel { final options = optionsRaw is List ? optionsRaw.map>((e) { if (e is Map) { - return Map.from(e as Map); + return Map.from(e); } else { return {}; } @@ -112,7 +113,10 @@ class EventModel { disassemblyTime: map['DisassemblyTime'] ?? 0, eventTypeId: map['EventType'] is DocumentReference ? (map['EventType'] as DocumentReference).id - : '', + : map['EventType'] ?? '', + eventTypeRef: map['EventType'] is DocumentReference + ? map['EventType'] as DocumentReference + : null, customerId: map['customer'] is DocumentReference ? (map['customer'] as DocumentReference).id : '', @@ -135,8 +139,8 @@ class EventModel { 'BasePrice': basePrice, 'InstallationTime': installationTime, 'DisassemblyTime': disassemblyTime, - 'EventType': eventTypeId, - 'customer': customerId, + 'EventType': eventTypeId.isNotEmpty ? FirebaseFirestore.instance.collection('eventTypes').doc(eventTypeId) : null, + 'customer': customerId.isNotEmpty ? FirebaseFirestore.instance.collection('customers').doc(customerId) : null, 'Address': address, 'Position': GeoPoint(latitude, longitude), 'Latitude': latitude, diff --git a/em2rp/lib/models/event_type_model.dart b/em2rp/lib/models/event_type_model.dart new file mode 100644 index 0000000..1acb99d --- /dev/null +++ b/em2rp/lib/models/event_type_model.dart @@ -0,0 +1,32 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class EventType { + final String id; + final String name; + final double defaultPrice; + + EventType({ + required this.id, + required this.name, + required this.defaultPrice, + }); + + factory EventType.fromFirestore(DocumentSnapshot doc) { + Map data = doc.data() as Map; + + double price = 0.0; + final priceData = data['defaultPrice']; + if (priceData is num) { + price = priceData.toDouble(); + } else if (priceData is String) { + price = double.tryParse(priceData.replaceAll(',', '.')) ?? 0.0; + } + + return EventType( + id: doc.id, + name: data['name'] ?? '', + defaultPrice: price, + ); + } +} + diff --git a/em2rp/lib/models/role_model.dart b/em2rp/lib/models/role_model.dart index e82be67..dff19f7 100644 --- a/em2rp/lib/models/role_model.dart +++ b/em2rp/lib/models/role_model.dart @@ -1,4 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; class RoleModel { final String id; diff --git a/em2rp/lib/providers/users_provider.dart b/em2rp/lib/providers/users_provider.dart index 2453d65..483dec4 100644 --- a/em2rp/lib/providers/users_provider.dart +++ b/em2rp/lib/providers/users_provider.dart @@ -3,7 +3,6 @@ import '../models/user_model.dart'; import '../services/user_service.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter/foundation.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/providers/local_user_provider.dart'; @@ -130,7 +129,7 @@ class UsersProvider with ChangeNotifier { await _auth.sendPasswordResetEmail( email: user.email, actionCodeSettings: ActionCodeSettings( - url: 'http://localhost:63337/finishSignUp?email=${user.email}', + url: 'http://app.em2events.fr/finishSignUp?email=${user.email}', handleCodeInApp: true, androidPackageName: 'com.em2rp.app', androidInstallApp: true, diff --git a/em2rp/lib/services/event_form_service.dart b/em2rp/lib/services/event_form_service.dart new file mode 100644 index 0000000..bc147c6 --- /dev/null +++ b/em2rp/lib/services/event_form_service.dart @@ -0,0 +1,143 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/models/event_type_model.dart'; +import 'package:em2rp/models/user_model.dart'; +import 'dart:developer' as developer; + +class EventFormService { + static Future> fetchEventTypes() async { + developer.log('Fetching event types from Firestore...', name: 'EventFormService'); + try { + final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get(); + final eventTypes = snapshot.docs.map((doc) => EventType.fromFirestore(doc)).toList(); + developer.log('${eventTypes.length} event types loaded.', name: 'EventFormService'); + return eventTypes; + } catch (e, s) { + developer.log('Error fetching event types', name: 'EventFormService', error: e, stackTrace: s); + throw Exception("Could not load event types. Please check Firestore permissions."); + } + } + + static Future> fetchUsers() async { + try { + final snapshot = await FirebaseFirestore.instance.collection('users').get(); + return snapshot.docs.map((doc) => UserModel.fromMap(doc.data(), doc.id)).toList(); + } catch (e) { + developer.log('Error fetching users', name: 'EventFormService', error: e); + throw Exception("Could not load users."); + } + } + + static Future>> uploadFiles(List files) async { + List> uploadedFiles = []; + + for (final file in files) { + final fileBytes = file.bytes; + final fileName = file.name; + + if (fileBytes != null) { + final ref = FirebaseStorage.instance.ref().child( + 'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName'); + final uploadTask = await ref.putData(fileBytes); + final url = await uploadTask.ref.getDownloadURL(); + uploadedFiles.add({'name': fileName, 'url': url}); + } else { + throw Exception("Impossible de lire le fichier $fileName"); + } + } + + return uploadedFiles; + } + + static Future moveEventFileHttp({ + required String sourcePath, + required String destinationPath, + }) async { + final url = Uri.parse('https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2'); + final user = FirebaseAuth.instance.currentUser; + final idToken = await user?.getIdToken(); + + final response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + if (idToken != null) 'Authorization': 'Bearer $idToken', + }, + body: jsonEncode({ + 'data': { + 'sourcePath': sourcePath, + 'destinationPath': destinationPath, + } + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data['url'] != null) { + return data['url'] as String; + } else if (data['result'] != null && data['result']['url'] != null) { + return data['result']['url'] as String; + } + return null; + } else { + print('Erreur Cloud Function: \n${response.body}'); + return null; + } + } + + static Future createEvent(EventModel event) async { + final docRef = await FirebaseFirestore.instance.collection('events').add(event.toMap()); + return docRef.id; + } + + static Future updateEvent(EventModel event) async { + final docRef = FirebaseFirestore.instance.collection('events').doc(event.id); + await docRef.update(event.toMap()); + } + + static Future>> moveFilesToEvent( + List> tempFiles, String eventId) async { + List> newFiles = []; + + for (final file in tempFiles) { + final fileName = file['name']!; + final oldUrl = file['url']!; + + String sourcePath; + final tempPattern = RegExp(r'events/temp/[^?]+'); + final match = tempPattern.firstMatch(oldUrl); + if (match != null) { + sourcePath = match.group(0)!; + } else { + final tempFileName = Uri.decodeComponent(oldUrl.split('/').last.split('?').first); + sourcePath = tempFileName; + } + + final destinationPath = 'events/$eventId/$fileName'; + final newUrl = await moveEventFileHttp( + sourcePath: sourcePath, + destinationPath: destinationPath, + ); + + if (newUrl != null) { + newFiles.add({'name': fileName, 'url': newUrl}); + } else { + newFiles.add({'name': fileName, 'url': oldUrl}); + } + } + + return newFiles; + } + + static Future updateEventDocuments(String eventId, List> documents) async { + await FirebaseFirestore.instance + .collection('events') + .doc(eventId) + .update({'documents': documents}); + } +} diff --git a/em2rp/lib/utils/permission_gate.dart b/em2rp/lib/utils/permission_gate.dart index b7f398c..e72f951 100644 --- a/em2rp/lib/utils/permission_gate.dart +++ b/em2rp/lib/utils/permission_gate.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:em2rp/models/role_model.dart'; import 'package:em2rp/providers/local_user_provider.dart'; class PermissionGate extends StatelessWidget { diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index d090ae8..dbaa4ec 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -120,22 +120,7 @@ class _CalendarPageState extends State { return Scaffold( appBar: CustomAppBar( - title: _getMonthName(_focusedDay.month), - actions: [ - IconButton( - icon: Icon( - _calendarCollapsed - ? Icons.keyboard_arrow_down - : Icons.keyboard_arrow_up, - color: AppColors.blanc, - ), - onPressed: () { - setState(() { - _calendarCollapsed = !_calendarCollapsed; - }); - }, - ), - ], + title: "Calendrier", ), drawer: const MainDrawer(currentPage: '/calendar'), body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(), @@ -149,8 +134,8 @@ class _CalendarPageState extends State { ), ); }, - child: const Icon(Icons.add, color: Colors.red), tooltip: 'Ajouter un événement', + child: const Icon(Icons.add, color: Colors.red), ) : null, ); @@ -252,7 +237,7 @@ class _CalendarPageState extends State { left: 0, right: 0, height: _calendarCollapsed ? 0 : null, - child: Container( + child: SizedBox( height: MediaQuery.of(context).size.height, child: Column( children: [ @@ -360,7 +345,7 @@ class _CalendarPageState extends State { left: 0, right: 0, bottom: 0, - child: Container( + child: SizedBox( height: MediaQuery.of(context).size.height, child: Column( children: [ diff --git a/em2rp/lib/views/event_add_page.dart b/em2rp/lib/views/event_add_page.dart index 8e322dd..4690814 100644 --- a/em2rp/lib/views/event_add_page.dart +++ b/em2rp/lib/views/event_add_page.dart @@ -1,27 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:em2rp/providers/event_provider.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'; -import 'package:em2rp/models/user_model.dart'; -import 'package:em2rp/views/widgets/image/profile_picture.dart'; -import 'package:flutter/services.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:path/path.dart' as p; -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:em2rp/providers/local_user_provider.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_dropzone/flutter_dropzone.dart'; -import 'package:em2rp/views/widgets/inputs/dropzone_upload_widget.dart'; -import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; +import 'package:em2rp/controllers/event_form_controller.dart'; +import 'package:em2rp/views/widgets/event_form/event_basic_info_section.dart'; +import 'package:em2rp/views/widgets/event_form/event_details_section.dart'; +import 'package:em2rp/views/widgets/event_form/event_staff_and_documents_section.dart'; +import 'package:em2rp/views/widgets/event_form/event_form_actions.dart'; import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart'; -// ignore: avoid_web_libraries_in_flutter -import 'dart:html' as html; +import 'package:flutter/foundation.dart'; class EventAddEditPage extends StatefulWidget { final EventModel? event; @@ -33,166 +19,27 @@ class EventAddEditPage extends StatefulWidget { class _EventAddEditPageState extends State { final _formKey = GlobalKey(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _descriptionController = TextEditingController(); - final TextEditingController _basePriceController = TextEditingController(); - final TextEditingController _installationController = TextEditingController(); - final TextEditingController _disassemblyController = TextEditingController(); - final TextEditingController _addressController = TextEditingController(); - DateTime? _startDateTime; - DateTime? _endDateTime; - bool _isLoading = false; - String? _error; - String? _success; - String? _selectedEventType; - final List _eventTypes = ['Bal', 'Mariage', 'Anniversaire']; - final Map _eventTypeDefaultPrices = { - 'Bal': 800.0, - 'Mariage': 1500.0, - 'Anniversaire': 500.0, - }; - int _descriptionMaxLines = 3; - List _selectedUserIds = []; - List _allUsers = []; - bool _isLoadingUsers = true; - List> _uploadedFiles = []; - DropzoneViewController? _dropzoneController; - bool _isDropzoneHighlighted = false; - List> _selectedOptions = []; - bool _formChanged = false; - EventStatus _selectedStatus = EventStatus.waitingForApproval; + late EventFormController _controller; bool get isEditMode => widget.event != null; @override void initState() { super.initState(); - _descriptionController.addListener(_handleDescriptionChange); - _fetchUsers(); - _nameController.addListener(_onAnyFieldChanged); - _basePriceController.addListener(_onAnyFieldChanged); - _installationController.addListener(_onAnyFieldChanged); - _disassemblyController.addListener(_onAnyFieldChanged); - _addressController.addListener(_onAnyFieldChanged); - _descriptionController.addListener(_onAnyFieldChanged); - _addBeforeUnloadListener(); - if (isEditMode) { - final e = widget.event!; - _nameController.text = e.name; - _descriptionController.text = e.description; - _basePriceController.text = e.basePrice.toStringAsFixed(2); - _installationController.text = e.installationTime.toString(); - _disassemblyController.text = e.disassemblyTime.toString(); - _addressController.text = e.address; - _startDateTime = e.startDateTime; - _endDateTime = e.endDateTime; - _selectedEventType = e.eventTypeId.isNotEmpty ? e.eventTypeId : null; - _selectedUserIds = e.workforce.map((ref) => ref.id).toList(); - _uploadedFiles = List>.from(e.documents); - _selectedOptions = List>.from(e.options); - _selectedStatus = e.status; - } else { - _selectedStatus = EventStatus.waitingForApproval; - } - } - - void _handleDescriptionChange() { - final lines = '\n'.allMatches(_descriptionController.text).length + 1; - setState(() { - _descriptionMaxLines = lines.clamp(3, 6); - }); - } - - void _onAnyFieldChanged() { - if (!_formChanged) { - setState(() { - _formChanged = true; - }); - } - } - - Future _fetchUsers() async { - final snapshot = await FirebaseFirestore.instance.collection('users').get(); - setState(() { - _allUsers = snapshot.docs - .map((doc) => UserModel.fromMap(doc.data(), doc.id)) - .toList(); - _isLoadingUsers = false; - }); - } - - void _onEventTypeChanged(String? newType) { - if (newType == _selectedEventType) return; - setState(() { - _selectedEventType = newType; - if (newType != null) { - // Appliquer le prix par défaut si champ vide ou si type changé - final defaultPrice = _eventTypeDefaultPrices[newType] ?? 0.0; - if (_basePriceController.text.isEmpty || - (_selectedEventType != null && - _basePriceController.text == - (_eventTypeDefaultPrices[_selectedEventType] ?? '') - .toString())) { - _basePriceController.text = defaultPrice.toStringAsFixed(2); - } - // Efface les options non compatibles - final before = _selectedOptions.length; - _selectedOptions.removeWhere((opt) { - final types = opt['compatibleTypes'] as List?; - if (types == null) return true; - return !types.contains(newType); - }); - if (_selectedOptions.length < before && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Certaines options ont été retirées car elles ne sont pas compatibles avec le type "$newType".')), - ); - } - } else { - _selectedOptions.clear(); - } - _onAnyFieldChanged(); - }); + _controller = EventFormController(); + _controller.initialize(widget.event); } @override void dispose() { - _nameController.dispose(); - _descriptionController.dispose(); - _basePriceController.dispose(); - _installationController.dispose(); - _disassemblyController.dispose(); - _addressController.dispose(); - _removeBeforeUnloadListener(); + _controller.dispose(); super.dispose(); } - // --- Web: beforeunload pour empêcher la fermeture sans confirmation --- - void _addBeforeUnloadListener() { - if (kIsWeb) { - html.window.onBeforeUnload.listen(_beforeUnloadHandler); - } - } - - void _removeBeforeUnloadListener() { - if (kIsWeb) { - // Il n'est pas possible de retirer un listener anonyme, donc on ne fait rien ici. - // Pour une gestion plus fine, il faudrait stocker la référence du listener. - } - } - - void _beforeUnloadHandler(html.Event event) { - if (_formChanged) { - event.preventDefault(); - // Pour Chrome/Edge/Firefox, il faut définir returnValue - // ignore: unsafe_html - (event as dynamic).returnValue = ''; - } - } - Future _onWillPop() async { - if (!_formChanged) return true; + if (!_controller.formChanged) return true; + if (!mounted) return true; + final shouldLeave = await showDialog( context: context, builder: (context) => AlertDialog( @@ -214,262 +61,59 @@ class _EventAddEditPageState extends State { return shouldLeave ?? false; } - Future _pickAndUploadFiles() async { - final result = await FilePicker.platform - .pickFiles(allowMultiple: true, withData: true); - if (result != null && result.files.isNotEmpty) { - setState(() => _isLoading = true); - try { - List> files = []; - for (final file in result.files) { - final fileBytes = file.bytes; - final fileName = file.name; - if (fileBytes != null) { - final ref = FirebaseStorage.instance.ref().child( - 'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName'); - final uploadTask = await ref.putData(fileBytes); - final url = await uploadTask.ref.getDownloadURL(); - files.add({'name': fileName, 'url': url}); - } else { - setState(() { - _error = "Impossible de lire le fichier ${file.name}"; - }); - } - } - setState(() { - _uploadedFiles.addAll(files); - }); - } catch (e) { - setState(() { - _error = 'Erreur lors de l\'upload : $e'; - }); - } finally { - setState(() => _isLoading = false); - } - } - } - - Future moveEventFileHttp({ - required String sourcePath, - required String destinationPath, - }) async { - final url = Uri.parse( - 'https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2'); - final user = FirebaseAuth.instance.currentUser; - final idToken = await user?.getIdToken(); - final response = await http.post( - url, - headers: { - 'Content-Type': 'application/json', - if (idToken != null) 'Authorization': 'Bearer $idToken', - }, - body: jsonEncode({ - 'data': { - 'sourcePath': sourcePath, - 'destinationPath': destinationPath, - } - }), - ); - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - if (data['url'] != null) { - return data['url'] as String; - } else if (data['result'] != null && data['result']['url'] != null) { - return data['result']['url'] as String; - } - return null; - } else { - print('Erreur Cloud Function: \\n${response.body}'); - return null; - } - } - Future _submit() async { - if (!_formKey.currentState!.validate() || - _startDateTime == null || - _endDateTime == null || - _selectedEventType == null || - _addressController.text.isEmpty) return; - if (_endDateTime!.isBefore(_startDateTime!) || - _endDateTime!.isAtSameMomentAs(_startDateTime!)) { - setState(() { - _error = "La date de fin doit être postérieure à la date de début."; - }); + if (!_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Veuillez remplir tous les champs obligatoires.'), + backgroundColor: Colors.red, + ), + ); return; } - setState(() { - _isLoading = true; - _error = null; - _success = null; - }); - try { - final eventProvider = Provider.of(context, listen: false); - if (isEditMode) { - // Edition : on met à jour l'événement existant - final updatedEvent = EventModel( - id: widget.event!.id, - name: _nameController.text.trim(), - description: _descriptionController.text.trim(), - startDateTime: _startDateTime!, - endDateTime: _endDateTime!, - basePrice: double.tryParse(_basePriceController.text) ?? 0.0, - installationTime: int.tryParse(_installationController.text) ?? 0, - disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0, - eventTypeId: _selectedEventType!, - customerId: '', - address: _addressController.text.trim(), - workforce: _selectedUserIds - .map((id) => - FirebaseFirestore.instance.collection('users').doc(id)) - .toList(), - latitude: 0.0, - longitude: 0.0, - documents: _uploadedFiles, - options: _selectedOptions - .map((opt) => { - 'name': opt['name'], - 'price': opt['price'], - }) - .toList(), - status: _selectedStatus, - ); - final docRef = FirebaseFirestore.instance - .collection('events') - .doc(widget.event!.id); - await docRef.update(updatedEvent.toMap()); - // Gestion des fichiers (si besoin, à adapter selon ta logique) - // ... - setState(() { - _success = "Événement modifié avec succès !"; - }); - if (context.mounted) Navigator.of(context).pop(); - } else { - // Création : logique existante - final newEvent = EventModel( - id: '', - name: _nameController.text.trim(), - description: _descriptionController.text.trim(), - startDateTime: _startDateTime!, - endDateTime: _endDateTime!, - basePrice: double.tryParse(_basePriceController.text) ?? 0.0, - installationTime: int.tryParse(_installationController.text) ?? 0, - disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0, - eventTypeId: _selectedEventType!, - customerId: '', - address: _addressController.text.trim(), - workforce: _selectedUserIds - .map((id) => - FirebaseFirestore.instance.collection('users').doc(id)) - .toList(), - latitude: 0.0, - longitude: 0.0, - documents: _uploadedFiles, - options: _selectedOptions - .map((opt) => { - 'name': opt['name'], - 'price': opt['price'], - }) - .toList(), - status: _selectedStatus, - ); - final docRef = await FirebaseFirestore.instance - .collection('events') - .add(newEvent.toMap()); - final eventId = docRef.id; - List> newFiles = []; - for (final file in _uploadedFiles) { - final fileName = file['name']!; - final oldUrl = file['url']!; - String sourcePath; - final tempPattern = RegExp(r'events/temp/[^?]+'); - final match = tempPattern.firstMatch(oldUrl); - if (match != null) { - sourcePath = match.group(0)!; - } else { - final tempFileName = - Uri.decodeComponent(oldUrl.split('/').last.split('?').first); - sourcePath = tempFileName; - } - final destinationPath = 'events/$eventId/$fileName'; - final newUrl = await moveEventFileHttp( - sourcePath: sourcePath, - destinationPath: destinationPath, - ); - if (newUrl != null) { - newFiles.add({'name': fileName, 'url': newUrl}); - } else { - newFiles.add({'name': fileName, 'url': oldUrl}); - } - } - await docRef.update({'documents': newFiles}); - final localUserProvider = - Provider.of(context, listen: false); - final userId = localUserProvider.uid; - final canViewAllEvents = - localUserProvider.hasPermission('view_all_events'); - if (userId != null) { - await eventProvider.loadUserEvents(userId, - canViewAllEvents: canViewAllEvents); - } - setState(() { - _success = "Événement créé avec succès !"; - }); - if (context.mounted) Navigator.of(context).pop(); - } - } catch (e) { - setState(() { - _error = "Erreur lors de la sauvegarde : $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), - ), - ), - ); + final success = await _controller.submitForm(context, existingEvent: widget.event); + if (success && mounted) { + Navigator.of(context).pop(); + } } @override Widget build(BuildContext context) { - final theme = Theme.of(context); final isMobile = MediaQuery.of(context).size.width < 600; - return WillPopScope( - onWillPop: _onWillPop, - child: Scaffold( - appBar: AppBar( - title: - Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'), - ), - body: Center( - child: SingleChildScrollView( - child: (isMobile - ? Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - child: _buildFormContent(isMobile), - ) - : Card( - elevation: 6, - margin: const EdgeInsets.all(24), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18)), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, vertical: 32), + + return ChangeNotifierProvider.value( + value: _controller, + child: PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + final shouldPop = await _onWillPop(); + if (shouldPop && mounted) { + Navigator.of(context).pop(); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'), + ), + body: Center( + child: SingleChildScrollView( + child: (isMobile + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: _buildFormContent(isMobile), - ), - )), + ) + : 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: _buildFormContent(isMobile), + ), + )), + ), ), ), ), @@ -477,373 +121,105 @@ class _EventAddEditPageState extends State { } Widget _buildFormContent(bool isMobile) { - return 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( - value: _selectedEventType, - items: _eventTypes - .map((type) => DropdownMenuItem( - value: type, - child: Text(type), - )) - .toList(), - onChanged: _onEventTypeChanged, - 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( + return Consumer( + builder: (context, controller, child) { + // Trouver le nom du type d'événement pour le passer au sélecteur d'options + final selectedEventTypeIndex = controller.selectedEventTypeId != null + ? controller.eventTypes.indexWhere((et) => et.id == controller.selectedEventTypeId) + : -1; + final selectedEventType = selectedEventTypeIndex != -1 + ? controller.eventTypes[selectedEventTypeIndex] + : null; + final selectedEventTypeName = selectedEventType?.name; + + return Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, 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, - ); - if (_endDateTime != null && - (_endDateTime!.isBefore(_startDateTime!) || - _endDateTime! - .isAtSameMomentAs(_startDateTime!))) { - _endDateTime = null; - } - }); - } - } - }, - 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, - ), + EventBasicInfoSection( + nameController: controller.nameController, + basePriceController: controller.basePriceController, + eventTypes: controller.eventTypes, + isLoadingEventTypes: controller.isLoadingEventTypes, + selectedEventTypeId: controller.selectedEventTypeId, + startDateTime: controller.startDateTime, + endDateTime: controller.endDateTime, + onEventTypeChanged: (typeId) => controller.onEventTypeChanged(typeId, context), + onStartDateTimeChanged: controller.setStartDateTime, + onEndDateTimeChanged: controller.setEndDateTime, + onAnyFieldChanged: () {}, // Géré automatiquement par le contrôleur + ), + const SizedBox(height: 16), + OptionSelectorWidget( + eventType: selectedEventTypeName, + selectedOptions: controller.selectedOptions, + onChanged: controller.setSelectedOptions, + onRemove: (name) { + final newOptions = List>.from(controller.selectedOptions); + newOptions.removeWhere((o) => o['name'] == name); + controller.setSelectedOptions(newOptions); + }, + eventTypeRequired: controller.selectedEventTypeId == null, + isMobile: isMobile, + ), + EventDetailsSection( + descriptionController: controller.descriptionController, + installationController: controller.installationController, + disassemblyController: controller.disassemblyController, + addressController: controller.addressController, + isMobile: isMobile, + onAnyFieldChanged: () {}, // Géré automatiquement par le contrôleur + ), + EventStaffAndDocumentsSection( + allUsers: controller.allUsers, + selectedUserIds: controller.selectedUserIds, + onUserSelectionChanged: controller.setSelectedUserIds, + isLoadingUsers: controller.isLoadingUsers, + uploadedFiles: controller.uploadedFiles, + onFilesChanged: controller.setUploadedFiles, + isLoading: controller.isLoading, + error: controller.error, + success: controller.success, + isMobile: isMobile, + onPickAndUploadFiles: controller.pickAndUploadFiles, + ), + if (controller.error != null) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + controller.error!, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, ), ), - ), - const SizedBox(width: 16), - Expanded( - child: GestureDetector( - onTap: _startDateTime == null - ? null - : () async { - final picked = await showDatePicker( - context: context, - initialDate: - _startDateTime!.add(const Duration(hours: 1)), - firstDate: _startDateTime!, - 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' - : (_startDateTime != null && - _endDateTime != null && - (_endDateTime!.isBefore(_startDateTime!) || - _endDateTime! - .isAtSameMomentAs(_startDateTime!))) - ? 'La date de fin doit être après la date de début' - : null, - ), + if (controller.success != null) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + controller.success!, + style: const TextStyle(color: Colors.green), + textAlign: TextAlign.center, ), ), + EventFormActions( + isLoading: controller.isLoading, + isEditMode: isEditMode, + onCancel: () async { + final shouldLeave = await _onWillPop(); + if (shouldLeave && mounted) { + Navigator.of(context).pop(); + } + }, + onSubmit: _submit, + onSetConfirmed: !isEditMode ? () { + } : null, ), ], ), - const SizedBox(height: 16), - TextFormField( - controller: _basePriceController, - decoration: const InputDecoration( - labelText: 'Prix de base (€)', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.euro), - hintText: '1050.50', - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')), - ], - validator: (value) { - if (value == null || value.isEmpty) { - return 'Le prix de base est requis'; - } - final price = double.tryParse(value.replaceAll(',', '.')); - if (price == null) { - return 'Veuillez entrer un nombre valide'; - } - return null; - }, - onChanged: (_) => _onAnyFieldChanged(), - ), - const SizedBox(height: 16), - OptionSelectorWidget( - eventType: _selectedEventType, - selectedOptions: _selectedOptions, - onChanged: (opts) => setState(() => _selectedOptions = opts), - onRemove: (name) { - setState(() { - _selectedOptions.removeWhere((o) => o['name'] == name); - }); - }, - eventTypeRequired: _selectedEventType == null, - isMobile: isMobile, - ), - _buildSectionTitle('Détails'), - AnimatedContainer( - duration: const Duration(milliseconds: 200), - constraints: BoxConstraints( - minHeight: 48, - maxHeight: isMobile ? 48.0 * 20 : 48.0 * 10, - ), - child: TextFormField( - controller: _descriptionController, - minLines: 1, - maxLines: _descriptionMaxLines > (isMobile ? 20 : 10) - ? (isMobile ? 20 : 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: 99, - ), - ), - const SizedBox(width: 16), - Expanded( - child: IntStepperField( - label: 'Démontage (h)', - controller: _disassemblyController, - min: 0, - max: 99, - ), - ), - ], - ), - _buildSectionTitle('Adresse'), - TextFormField( - controller: _addressController, - decoration: const InputDecoration( - labelText: 'Adresse', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.location_on), - ), - validator: (v) => v == null || v.isEmpty ? 'Champ requis' : null, - ), - _buildSectionTitle('Personnel'), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: UserMultiSelectWidget( - allUsers: _allUsers, - selectedUserIds: _selectedUserIds, - onChanged: (ids) => setState(() => _selectedUserIds = ids), - isLoading: _isLoadingUsers, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionTitle('Documents'), - if (isMobile) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ElevatedButton.icon( - icon: const Icon(Icons.attach_file), - label: const Text('Ajouter un fichier'), - onPressed: _isLoading ? null : _pickAndUploadFiles, - ), - const SizedBox(height: 8), - ..._uploadedFiles.map((file) => ListTile( - dense: true, - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.insert_drive_file), - title: Text(file['name'] ?? ''), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: _isLoading - ? null - : () { - setState(() { - _uploadedFiles.remove(file); - }); - }, - ), - )), - if (_error != null) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text(_error!, - style: const TextStyle(color: Colors.red)), - ), - if (_success != null) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text(_success!, - style: const TextStyle(color: Colors.green)), - ), - ], - ), - if (!isMobile) - DropzoneUploadWidget( - uploadedFiles: _uploadedFiles, - onFilesChanged: (files) => - setState(() => _uploadedFiles = files), - isLoading: _isLoading, - error: _error, - success: _success, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: _isLoading - ? null - : () async { - final shouldLeave = await _onWillPop(); - if (shouldLeave && context.mounted) { - 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), - ) - : Text(isEditMode ? 'Enregistrer' : 'Créer'), - ), - ], - ), - if (!isEditMode) - Center( - child: ElevatedButton.icon( - icon: const Icon(Icons.check_circle, color: Colors.white), - label: const Text('Définir cet événement comme confirmé'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - textStyle: const TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: null, - ), - ), - ], - ), + ); + }, ); } } diff --git a/em2rp/lib/views/user_management_page.dart b/em2rp/lib/views/user_management_page.dart index ab4a954..56afdf8 100644 --- a/em2rp/lib/views/user_management_page.dart +++ b/em2rp/lib/views/user_management_page.dart @@ -115,7 +115,7 @@ class _UserManagementPageState extends State { List availableRoles = []; bool isLoadingRoles = true; - Future _loadRoles() async { + Future loadRoles() async { final snapshot = await FirebaseFirestore.instance.collection('roles').get(); availableRoles = snapshot.docs @@ -145,7 +145,7 @@ class _UserManagementPageState extends State { showDialog( context: context, builder: (context) => FutureBuilder( - future: _loadRoles(), + future: loadRoles(), builder: (context, snapshot) { return Dialog( shape: RoundedRectangleBorder( @@ -204,7 +204,7 @@ class _UserManagementPageState extends State { isLoadingRoles ? const CircularProgressIndicator() : DropdownButtonFormField( - value: selectedRoleId, + initialValue: selectedRoleId, decoration: buildInputDecoration('Rôle', Icons.admin_panel_settings_outlined), items: availableRoles.map((role) { diff --git a/em2rp/lib/views/widgets/auth/forgot_password_dialog.dart b/em2rp/lib/views/widgets/auth/forgot_password_dialog.dart index 64df7ce..47bdc12 100644 --- a/em2rp/lib/views/widgets/auth/forgot_password_dialog.dart +++ b/em2rp/lib/views/widgets/auth/forgot_password_dialog.dart @@ -81,7 +81,7 @@ class _ForgotPasswordDialogState extends State { ), actions: [ TextButton( - child: const Text('Terminer'), + child: const Text('Annuler'), onPressed: () { Navigator.of(context).pop(); }, diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart index d86aca7..49fb76a 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart @@ -87,13 +87,31 @@ class EventDetails extends StatelessWidget { ), const SizedBox(height: 16), Row( + crossAxisAlignment: CrossAxisAlignment.start, // Optionnel mais recommandé pour bien aligner children: [ - SelectableText( - event.name, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: AppColors.noir, - fontWeight: FontWeight.bold, + // On remplace le SelectableText par une Column + Expanded( // Utiliser Expanded pour que le texte ne déborde pas + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, // Aligne les textes à gauche + children: [ + // 1. Votre titre original + SelectableText( + event.name, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: AppColors.noir, + fontWeight: FontWeight.bold, + ), ), + + const SizedBox(height: 4), + Text( + event.eventTypeId, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppColors.rouge, + ), + ), + ], + ), ), const SizedBox(width: 12), _buildStatusIcon(event.status), @@ -119,7 +137,7 @@ class EventDetails extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: _FirestoreStatusButton( - eventId: event.id, + eventId: event.id, currentStatus: event.status, onStatusChanged: (newStatus) async { await FirebaseFirestore.instance diff --git a/em2rp/lib/views/widgets/event_form/event_basic_info_section.dart b/em2rp/lib/views/widgets/event_form/event_basic_info_section.dart new file mode 100644 index 0000000..4b162ee --- /dev/null +++ b/em2rp/lib/views/widgets/event_form/event_basic_info_section.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:em2rp/models/event_type_model.dart'; + +class EventBasicInfoSection extends StatelessWidget { + final TextEditingController nameController; + final TextEditingController basePriceController; + final List eventTypes; + final bool isLoadingEventTypes; + final String? selectedEventTypeId; + final DateTime? startDateTime; + final DateTime? endDateTime; + final Function(String?) onEventTypeChanged; + final Function(DateTime?) onStartDateTimeChanged; + final Function(DateTime?) onEndDateTimeChanged; + final VoidCallback onAnyFieldChanged; + + const EventBasicInfoSection({ + super.key, + required this.nameController, + required this.basePriceController, + required this.eventTypes, + required this.isLoadingEventTypes, + required this.selectedEventTypeId, + required this.startDateTime, + required this.endDateTime, + required this.onEventTypeChanged, + required this.onStartDateTimeChanged, + required this.onEndDateTimeChanged, + required this.onAnyFieldChanged, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSectionTitle('Informations principales'), + 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), + if (isLoadingEventTypes) + const Center(child: CircularProgressIndicator()) + else + DropdownButtonFormField( + value: selectedEventTypeId, + items: eventTypes + .map((type) => DropdownMenuItem( + value: type.id, + child: Text(type.name), + )) + .toList(), + onChanged: onEventTypeChanged, + 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), + _buildDateTimeRow(context), + const SizedBox(height: 16), + TextFormField( + controller: basePriceController, + decoration: const InputDecoration( + labelText: 'Prix de base (€)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.euro), + hintText: '1050.50', + ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')), + ], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Le prix de base est requis'; + } + final price = double.tryParse(value.replaceAll(',', '.')); + if (price == null) { + return 'Veuillez entrer un nombre valide'; + } + return null; + }, + onChanged: (_) => onAnyFieldChanged(), + ), + ], + ); + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.only(top: 0.0, bottom: 4.0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ); + } + + Widget _buildDateTimeRow(BuildContext context) { + return Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => _selectStartDateTime(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + decoration: const InputDecoration( + labelText: 'Début', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.calendar_today), + suffixIcon: 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: startDateTime == null ? null : () => _selectEndDateTime(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + decoration: const InputDecoration( + labelText: 'Fin', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.calendar_today), + suffixIcon: Icon(Icons.edit_calendar), + ), + controller: TextEditingController( + text: endDateTime == null + ? '' + : DateFormat('dd/MM/yyyy HH:mm').format(endDateTime!), + ), + validator: (v) => endDateTime == null + ? 'Champ requis' + : (startDateTime != null && + endDateTime != null && + (endDateTime!.isBefore(startDateTime!) || + endDateTime!.isAtSameMomentAs(startDateTime!))) + ? 'La date de fin doit être après la date de début' + : null, + ), + ), + ), + ), + ], + ); + } + + Future _selectStartDateTime(BuildContext context) 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) { + final newDateTime = DateTime( + picked.year, + picked.month, + picked.day, + time.hour, + time.minute, + ); + onStartDateTimeChanged(newDateTime); + } + } + } + + Future _selectEndDateTime(BuildContext context) async { + final picked = await showDatePicker( + context: context, + initialDate: startDateTime!.add(const Duration(hours: 1)), + firstDate: startDateTime!, + lastDate: DateTime(2099), + ); + if (picked != null) { + final time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (time != null) { + final newDateTime = DateTime( + picked.year, + picked.month, + picked.day, + time.hour, + time.minute, + ); + onEndDateTimeChanged(newDateTime); + } + } + } +} diff --git a/em2rp/lib/views/widgets/event_form/event_details_section.dart b/em2rp/lib/views/widgets/event_form/event_details_section.dart new file mode 100644 index 0000000..274f033 --- /dev/null +++ b/em2rp/lib/views/widgets/event_form/event_details_section.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart'; + +class EventDetailsSection extends StatefulWidget { + final TextEditingController descriptionController; + final TextEditingController installationController; + final TextEditingController disassemblyController; + final TextEditingController addressController; + final bool isMobile; + final VoidCallback onAnyFieldChanged; + + const EventDetailsSection({ + super.key, + required this.descriptionController, + required this.installationController, + required this.disassemblyController, + required this.addressController, + required this.isMobile, + required this.onAnyFieldChanged, + }); + + @override + State createState() => _EventDetailsSectionState(); +} + +class _EventDetailsSectionState extends State { + int _descriptionMaxLines = 3; + + @override + void initState() { + super.initState(); + widget.descriptionController.addListener(_handleDescriptionChange); + } + + @override + void dispose() { + widget.descriptionController.removeListener(_handleDescriptionChange); + super.dispose(); + } + + void _handleDescriptionChange() { + final lines = '\n'.allMatches(widget.descriptionController.text).length + 1; + setState(() { + _descriptionMaxLines = lines.clamp(3, 6); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSectionTitle('Détails'), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + constraints: BoxConstraints( + minHeight: 48, + maxHeight: widget.isMobile ? 48.0 * 20 : 48.0 * 10, + ), + child: TextFormField( + controller: widget.descriptionController, + minLines: 1, + maxLines: _descriptionMaxLines > (widget.isMobile ? 20 : 10) + ? (widget.isMobile ? 20 : 10) + : _descriptionMaxLines, + decoration: const InputDecoration( + labelText: 'Description', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.description), + ), + onChanged: (_) => widget.onAnyFieldChanged(), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: IntStepperField( + label: 'Installation (h)', + controller: widget.installationController, + min: 0, + max: 99, + ), + ), + const SizedBox(width: 16), + Expanded( + child: IntStepperField( + label: 'Démontage (h)', + controller: widget.disassemblyController, + min: 0, + max: 99, + ), + ), + ], + ), + _buildSectionTitle('Adresse'), + TextFormField( + controller: widget.addressController, + decoration: const InputDecoration( + labelText: 'Adresse', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.location_on), + ), + validator: (v) => v == null || v.isEmpty ? 'Champ requis' : null, + onChanged: (_) => widget.onAnyFieldChanged(), + ), + ], + ); + } + + 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), + ), + ), + ); + } +} diff --git a/em2rp/lib/views/widgets/event_form/event_form_actions.dart b/em2rp/lib/views/widgets/event_form/event_form_actions.dart new file mode 100644 index 0000000..ef906bf --- /dev/null +++ b/em2rp/lib/views/widgets/event_form/event_form_actions.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class EventFormActions extends StatelessWidget { + final bool isLoading; + final bool isEditMode; + final VoidCallback onCancel; + final VoidCallback onSubmit; + final VoidCallback? onSetConfirmed; + + const EventFormActions({ + super.key, + required this.isLoading, + required this.isEditMode, + required this.onCancel, + required this.onSubmit, + this.onSetConfirmed, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: isLoading ? null : onCancel, + child: const Text('Annuler'), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + icon: const Icon(Icons.check), + onPressed: isLoading ? null : onSubmit, + label: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(isEditMode ? 'Enregistrer' : 'Créer'), + ), + ], + ), + if (!isEditMode && onSetConfirmed != null) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ElevatedButton.icon( + icon: const Icon(Icons.check_circle, color: Colors.white), + label: const Text('Définir cet événement comme confirmé'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + textStyle: const TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: onSetConfirmed, + ), + ), + ), + ], + ); + } +} diff --git a/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart b/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart new file mode 100644 index 0000000..e3680b9 --- /dev/null +++ b/em2rp/lib/views/widgets/event_form/event_staff_and_documents_section.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/user_model.dart'; +import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; +import 'package:em2rp/views/widgets/inputs/dropzone_upload_widget.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:firebase_storage/firebase_storage.dart'; + +class EventStaffAndDocumentsSection extends StatelessWidget { + final List allUsers; + final List selectedUserIds; + final Function(List) onUserSelectionChanged; + final bool isLoadingUsers; + final List> uploadedFiles; + final Function(List>) onFilesChanged; + final bool isLoading; + final String? error; + final String? success; + final bool isMobile; + final VoidCallback? onPickAndUploadFiles; + + const EventStaffAndDocumentsSection({ + super.key, + required this.allUsers, + required this.selectedUserIds, + required this.onUserSelectionChanged, + required this.isLoadingUsers, + required this.uploadedFiles, + required this.onFilesChanged, + required this.isLoading, + this.error, + this.success, + required this.isMobile, + this.onPickAndUploadFiles, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSectionTitle('Personnel'), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: UserMultiSelectWidget( + allUsers: allUsers, + selectedUserIds: selectedUserIds, + onChanged: onUserSelectionChanged, + isLoading: isLoadingUsers, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle('Documents'), + if (isMobile) + _buildMobileDocumentUpload() + else + DropzoneUploadWidget( + uploadedFiles: uploadedFiles, + onFilesChanged: onFilesChanged, + isLoading: isLoading, + error: error, + success: success, + ), + ], + ), + ), + ], + ), + ], + ); + } + + 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), + ), + ), + ); + } + + Widget _buildMobileDocumentUpload() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.attach_file), + label: const Text('Ajouter un fichier'), + onPressed: isLoading ? null : onPickAndUploadFiles, + ), + const SizedBox(height: 8), + ...uploadedFiles.map((file) => ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.insert_drive_file), + title: Text(file['name'] ?? ''), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: isLoading + ? null + : () { + final newFiles = List>.from(uploadedFiles); + newFiles.remove(file); + onFilesChanged(newFiles); + }, + ), + )), + if (error != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text(error!, style: const TextStyle(color: Colors.red)), + ), + if (success != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text(success!, style: const TextStyle(color: Colors.green)), + ), + ], + ); + } +} diff --git a/em2rp/lib/views/widgets/inputs/dropzone_upload_widget.dart b/em2rp/lib/views/widgets/inputs/dropzone_upload_widget.dart index c14d189..bd36961 100644 --- a/em2rp/lib/views/widgets/inputs/dropzone_upload_widget.dart +++ b/em2rp/lib/views/widgets/inputs/dropzone_upload_widget.dart @@ -49,16 +49,14 @@ class _DropzoneUploadWidgetState extends State { for (final file in files) { final name = await _dropzoneController!.getFilename(file); final bytes = await _dropzoneController!.getFileData(file); - if (bytes != null) { - final ref = FirebaseStorage.instance.ref().child( - 'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name'); - final uploadTask = await ref.putData(bytes); - final url = await uploadTask.ref.getDownloadURL(); - if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) { - newFiles.add({'name': name, 'url': url}); - } + final ref = FirebaseStorage.instance.ref().child( + 'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name'); + final uploadTask = await ref.putData(bytes); + final url = await uploadTask.ref.getDownloadURL(); + if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) { + newFiles.add({'name': name, 'url': url}); } - } + } widget.onFilesChanged(newFiles); setState(() { _success = "Fichier(s) ajouté(s) !"; @@ -233,7 +231,7 @@ class _DropzoneUploadWidgetState extends State { contentPadding: EdgeInsets.zero, dense: true, ); - }).toList(), + }), SizedBox( width: 160, child: ElevatedButton.icon( diff --git a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart index c6db444..1ffd6a1 100644 --- a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart +++ b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart @@ -27,7 +27,7 @@ class OptionSelectorWidget extends StatefulWidget { class _OptionSelectorWidgetState extends State { List _allOptions = []; bool _loading = true; - String _search = ''; + final String _search = ''; final List _eventTypes = ['Bal', 'Mariage', 'Anniversaire']; @override @@ -201,8 +201,7 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { final opt = filtered[i]; return ListTile( title: Text(opt.name), - subtitle: Text(opt.details + - '\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'), + subtitle: Text('${opt.details}\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'), onTap: () async { final min = opt.valMin; final max = opt.valMax; @@ -304,7 +303,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { final _detailsController = TextEditingController(); final _minPriceController = TextEditingController(); final _maxPriceController = TextEditingController(); - List _selectedTypes = []; + final List _selectedTypes = []; final List _allTypes = ['Bal', 'Mariage', 'Anniversaire']; String? _error; bool _checkingName = false; diff --git a/em2rp/lib/views/widgets/nav/main_drawer.dart b/em2rp/lib/views/widgets/nav/main_drawer.dart index 20c8ede..3e31caf 100644 --- a/em2rp/lib/views/widgets/nav/main_drawer.dart +++ b/em2rp/lib/views/widgets/nav/main_drawer.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:em2rp/views/widgets/image/profile_picture.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/utils/permission_gate.dart'; -import 'package:em2rp/models/role_model.dart'; class MainDrawer extends StatelessWidget { final String currentPage; diff --git a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart index c36a00f..a041908 100644 --- a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart +++ b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart @@ -129,7 +129,7 @@ class _EditUserDialogState extends State { isLoadingRoles ? const CircularProgressIndicator() : DropdownButtonFormField( - value: selectedRoleId, + initialValue: selectedRoleId, decoration: _buildInputDecoration( 'Rôle', Icons.admin_panel_settings_outlined), items: availableRoles.map((role) { diff --git a/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart b/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart index 76be450..bfd1116 100644 --- a/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart +++ b/em2rp/lib/views/widgets/user_management/user_multi_select_widget.dart @@ -38,7 +38,6 @@ class _UserMultiSelect extends StatefulWidget { final ValueChanged> onChanged; const _UserMultiSelect({ - super.key, required this.allUsers, required this.selectedUserIds, required this.onChanged, diff --git a/em2rp/pubspec.yaml b/em2rp/pubspec.yaml index 6e9dcae..838e29d 100644 --- a/em2rp/pubspec.yaml +++ b/em2rp/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: universal_io: ^2.2.2 cupertino_icons: ^1.0.2 table_calendar: ^3.0.9 - intl: ^0.19.0 + intl: ^0.20.2 google_maps_flutter: ^2.5.0 permission_handler: ^12.0.0+1 geolocator: ^14.0.1 @@ -55,6 +55,7 @@ dependencies: flutter_localizations: sdk: flutter + path: any dev_dependencies: flutter_test: sdk: flutter diff --git a/em2rp/web/index.html b/em2rp/web/index.html index 3b08ea3..690c172 100644 --- a/em2rp/web/index.html +++ b/em2rp/web/index.html @@ -27,7 +27,7 @@ - + em2rp