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/views/widgets/inputs/option_selector_widget.dart'; // ignore: avoid_web_libraries_in_flutter import 'dart:html' as html; class EventAddEditPage extends StatefulWidget { final EventModel? event; const EventAddEditPage({super.key, this.event}); @override State createState() => _EventAddEditPageState(); } 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; 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(); }); } @override void dispose() { _nameController.dispose(); _descriptionController.dispose(); _basePriceController.dispose(); _installationController.dispose(); _disassemblyController.dispose(); _addressController.dispose(); _removeBeforeUnloadListener(); 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; final shouldLeave = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Quitter la page ?'), content: const Text( 'Les modifications non enregistrées seront perdues. Voulez-vous vraiment quitter ?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Quitter'), ), ], ), ); 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."; }); 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), ), ), ); } @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), child: _buildFormContent(isMobile), ), )), ), ), ), ); } 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( 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, ), ), ), ), 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, ), ), ), ), ], ), 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, ), ), ], ), ); } }