From d9cd251bb7feb38a57bdaf31097cb903c35859fc Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Wed, 27 May 2026 23:50:02 +0200 Subject: [PATCH] feat: implement event creation flow and management widgets with preparation tracking buttons --- .../controllers/event_form_controller.dart | 5 + em2rp/lib/views/event_add_page.dart | 270 ++++++++++++------ .../event_preparation_buttons.dart | 22 +- .../event_assigned_equipment_section.dart | 245 ++++++++++------ .../event_form/event_basic_info_section.dart | 180 ++++++------ .../event_form/event_details_section.dart | 18 +- .../event_form/event_form_actions.dart | 115 ++++---- .../event_staff_and_documents_section.dart | 34 +-- .../event_form/price_ht_ttc_fields.dart | 80 +++++- 9 files changed, 588 insertions(+), 381 deletions(-) diff --git a/em2rp/lib/controllers/event_form_controller.dart b/em2rp/lib/controllers/event_form_controller.dart index 74c78ac..df0ccef 100644 --- a/em2rp/lib/controllers/event_form_controller.dart +++ b/em2rp/lib/controllers/event_form_controller.dart @@ -433,6 +433,11 @@ class EventFormController extends ChangeNotifier { } } + Future submitAsConfirmed(BuildContext context) async { + _selectedStatus = EventStatus.confirmed; + return await submitForm(context); + } + Future deleteEvent(BuildContext context, String eventId) async { _isLoading = true; _error = null; diff --git a/em2rp/lib/views/event_add_page.dart b/em2rp/lib/views/event_add_page.dart index 2620f95..4e81266 100644 --- a/em2rp/lib/views/event_add_page.dart +++ b/em2rp/lib/views/event_add_page.dart @@ -142,6 +142,54 @@ class _EventAddEditPageState extends State { } } + Widget _buildCard({ + required String title, + required IconData icon, + required List children, + }) { + return Card( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200, width: 1), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFFD32F2F).withOpacity(0.1), // AppColors.rouge + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: const Color(0xFFD32F2F), size: 22), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + ], + ), + const SizedBox(height: 24), + ...children, + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; @@ -158,29 +206,23 @@ class _EventAddEditPageState extends State { } }, child: Scaffold( + backgroundColor: Colors.grey.shade50, appBar: AppBar( title: Text( isEditMode ? 'Modifier un événement' : 'Créer un événement'), + elevation: 0, ), - 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), - ), - )), + body: SingleChildScrollView( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1200), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 16 : 32, + vertical: 32), + child: _buildFormContent(isMobile), + ), + ), ), ), ), @@ -197,38 +239,43 @@ class _EventAddEditPageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - 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 + _buildCard( + title: 'Informations générales', + icon: Icons.event_note, + children: [ + EventBasicInfoSection( + nameController: controller.nameController, + basePriceController: controller.basePriceController, + eventTypes: controller.eventTypes, + isLoadingEventTypes: controller.isLoadingEventTypes, + selectedEventTypeId: controller.selectedEventTypeId, + startDateTime: controller.startDateTime, + endDateTime: controller.endDateTime, + selectedOptions: controller.selectedOptions, + onEventTypeChanged: (typeId) => + controller.onEventTypeChanged(typeId, context), + onStartDateTimeChanged: controller.setStartDateTime, + onEndDateTimeChanged: controller.setEndDateTime, + onAnyFieldChanged: () {}, + ), + const SizedBox(height: 16), + OptionSelectorWidget( + eventType: controller.selectedEventTypeId, + selectedOptions: controller.selectedOptions, + onChanged: controller.setSelectedOptions, + onRemove: (optionId) { + final newOptions = List>.from( + controller.selectedOptions); + newOptions.removeWhere((o) => o['id'] == optionId); + controller.setSelectedOptions(newOptions); + }, + eventTypeRequired: controller.selectedEventTypeId == null, + isMobile: isMobile, + ), + ], ), - const SizedBox(height: 16), - OptionSelectorWidget( - eventType: controller - .selectedEventTypeId, // Utilise l'ID au lieu du nom - selectedOptions: controller.selectedOptions, - onChanged: controller.setSelectedOptions, - onRemove: (optionId) { - final newOptions = List>.from( - controller.selectedOptions); - newOptions.removeWhere((o) => o['id'] == optionId); - controller.setSelectedOptions(newOptions); - }, - eventTypeRequired: controller.selectedEventTypeId == null, - isMobile: isMobile, - ), - const SizedBox(height: 16), - // Section Matériel Assigné + const SizedBox(height: 24), + // Section Matériel Assigné (gère sa propre carte pour inclure les boutons d'action dans le header) EventAssignedEquipmentSection( assignedEquipment: controller.assignedEquipment, assignedContainers: controller.assignedContainers, @@ -238,50 +285,93 @@ class _EventAddEditPageState extends State { eventId: widget.event?.id, eventTypeId: controller.selectedEventTypeId, ), - const SizedBox(height: 16), - EventDetailsSection( - descriptionController: controller.descriptionController, - installationController: controller.installationController, - disassemblyController: controller.disassemblyController, - addressController: controller.addressController, - jaugeController: controller.jaugeController, - contactEmailController: controller.contactEmailController, - contactPhoneController: controller.contactPhoneController, - isMobile: isMobile, - onAnyFieldChanged: - () {}, // Géré automatiquement par le contrôleur + const SizedBox(height: 24), + _buildCard( + title: 'Détails & Logistique', + icon: Icons.location_on_outlined, + children: [ + EventDetailsSection( + descriptionController: controller.descriptionController, + installationController: controller.installationController, + disassemblyController: controller.disassemblyController, + addressController: controller.addressController, + jaugeController: controller.jaugeController, + contactEmailController: controller.contactEmailController, + contactPhoneController: controller.contactPhoneController, + isMobile: isMobile, + onAnyFieldChanged: () {}, + ), + ], ), - 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, + const SizedBox(height: 24), + _buildCard( + title: 'Personnel & Documents', + icon: Icons.group_outlined, + children: [ + 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, + padding: const EdgeInsets.only(top: 24.0), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200) + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: Colors.red.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + controller.error!, + style: TextStyle(color: Colors.red.shade700), + ), + ), + ], + ), ), ), 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, + padding: const EdgeInsets.only(top: 24.0), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200) + ), + child: Row( + children: [ + Icon(Icons.check_circle_outline, color: Colors.green.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + controller.success!, + style: TextStyle(color: Colors.green.shade700), + ), + ), + ], + ), ), ), + const SizedBox(height: 24), EventFormActions( isLoading: controller.isLoading, isEditMode: isEditMode, @@ -292,11 +382,15 @@ class _EventAddEditPageState extends State { } }, onSubmit: _submit, - onSetConfirmed: !isEditMode ? () {} : null, - onDelete: isEditMode - ? _deleteEvent - : null, // Ajout du callback de suppression + onSetConfirmed: !isEditMode ? () async { + final success = await controller.submitAsConfirmed(context); + if (success && context.mounted) { + Navigator.of(context).pop(); + } + } : null, + onDelete: isEditMode ? _deleteEvent : null, ), + const SizedBox(height: 48), // Padding bottom for scrolling ], ), ); diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart index dee3e8a..debe7f7 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart @@ -262,11 +262,18 @@ class _EventPreparationButtonsState extends State { ); if (confirm == true && context.mounted) { + // Utiliser le rootNavigator pour s'assurer qu'on gère la bonne pile de navigation + final navigator = Navigator.of(context, rootNavigator: true); + showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) { - return const Center(child: CircularProgressIndicator()); + useRootNavigator: true, + builder: (BuildContext dialogContext) { + return const PopScope( + canPop: false, + child: Center(child: CircularProgressIndicator()), + ); }, ); @@ -277,15 +284,21 @@ class _EventPreparationButtonsState extends State { 'targetStep': selectedStep, }); + // Attendre un tout petit peu pour s'assurer que le showDialog a eu le temps + // de faire son animation d'entrée si l'API a répondu trop vite. + await Future.delayed(const Duration(milliseconds: 200)); + if (context.mounted) { final eventProvider = Provider.of(context, listen: false); final userProvider = Provider.of(context, listen: false); if (userProvider.currentUser != null) { await eventProvider.refreshEvents(userProvider.currentUser!.uid); } + } - Navigator.of(context).pop(); // Fermer le loader + navigator.pop(); // Fermer le loader + if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Retour en arrière effectué avec succès'), @@ -294,8 +307,9 @@ class _EventPreparationButtonsState extends State { ); } } catch (e) { + navigator.pop(); // Fermer le loader + if (context.mounted) { - Navigator.of(context).pop(); // Fermer le loader ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur : $e'), diff --git a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart index 50d691f..8bff962 100644 --- a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart @@ -43,6 +43,7 @@ class _EventAssignedEquipmentSectionState final Map _equipmentCache = {}; final Map _containerCache = {}; bool _isLoading = true; + final ScrollController _scrollController = ScrollController(); @override void initState() { @@ -50,6 +51,12 @@ class _EventAssignedEquipmentSectionState _loadEquipmentAndContainers(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override void didUpdateWidget(EventAssignedEquipmentSection oldWidget) { super.didUpdateWidget(oldWidget); @@ -394,74 +401,93 @@ class _EventAssignedEquipmentSectionState widget.assignedEquipment.length + widget.assignedContainers.length; return Card( - elevation: 2, + elevation: 0, + color: Colors.white, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200, width: 1), ), child: Padding( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColors.rouge.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.inventory_2, - color: AppColors.rouge, - size: 24, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + SizedBox( + width: double.infinity, + child: Wrap( + spacing: 16, + runSpacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Matériel assigné', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.rouge.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.inventory_2, + color: AppColors.rouge, + size: 22, ), ), - Text( - '$totalItems élément(s)', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Matériel assigné', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + '$totalItems élément(s)', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ActionChip( + onPressed: _canAddMaterial ? _openAiAssistantDialog : null, + avatar: const Icon(Icons.auto_fix_high, size: 18), + label: const Text('Assistant IA'), + ), + ElevatedButton.icon( + onPressed: _canAddMaterial ? _openSelectionDialog : null, + icon: Icon(Icons.add, + color: _canAddMaterial ? Colors.white : Colors.grey), + label: Text( + 'Ajouter', + style: TextStyle( + color: _canAddMaterial ? Colors.white : Colors.grey), + ), + style: ElevatedButton.styleFrom( + backgroundColor: _canAddMaterial + ? AppColors.rouge + : Colors.grey.shade300, ), ), ], ), - ), - ActionChip( - onPressed: _canAddMaterial ? _openAiAssistantDialog : null, - avatar: const Icon(Icons.auto_fix_high, size: 18), - label: const Text('Assistant IA'), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: _canAddMaterial ? _openSelectionDialog : null, - icon: Icon(Icons.add, - color: _canAddMaterial ? Colors.white : Colors.grey), - label: Text( - 'Ajouter', - style: TextStyle( - color: _canAddMaterial ? Colors.white : Colors.grey), - ), - style: ElevatedButton.styleFrom( - backgroundColor: _canAddMaterial - ? AppColors.rouge - : Colors.grey.shade300, - ), - ), - ], + ], + ), ), // Message si dates non sélectionnées @@ -522,42 +548,83 @@ class _EventAssignedEquipmentSectionState ), ) else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Conteneurs - if (widget.assignedContainers.isNotEmpty) ...[ - Text( - 'Boîtes (${widget.assignedContainers.length})', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - ...widget.assignedContainers.map((containerId) { - final container = _containerCache[containerId]; - return _buildContainerItem(container); - }), - const SizedBox(height: 16), - ], + Container( + constraints: const BoxConstraints(maxHeight: 400), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(8), + color: Colors.grey.shade50, + ), + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + final hasContainers = widget.assignedContainers.isNotEmpty; + final hasEquipment = _getStandaloneEquipment().isNotEmpty; - // Équipements directs (qui ne sont PAS dans un conteneur assigné) - if (_getStandaloneEquipment().isNotEmpty) ...[ - Text( - 'Équipements (${_getStandaloneEquipment().length})', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - ..._getStandaloneEquipment().map((eq) { - final equipment = _equipmentCache[eq.equipmentId]; - return _buildEquipmentItem(equipment, eq); - }), - ], - ], + final containersContent = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasContainers) ...[ + Text( + 'Boîtes (${widget.assignedContainers.length})', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + ...widget.assignedContainers.map((containerId) { + final container = _containerCache[containerId]; + return _buildContainerItem(container); + }), + const SizedBox(height: 16), + ] + ], + ); + + final equipmentContent = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasEquipment) ...[ + Text( + 'Équipements (${_getStandaloneEquipment().length})', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + ..._getStandaloneEquipment().map((eq) { + final equipment = _equipmentCache[eq.equipmentId]; + return _buildEquipmentItem(equipment, eq); + }), + ] + ], + ); + + if (constraints.maxWidth > 600 && hasContainers && hasEquipment) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: containersContent), + const SizedBox(width: 24), + Expanded(child: equipmentContent), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + containersContent, + equipmentContent, + ], + ); + } + }, + ), + ), ), ], ), 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 index 8ab92e5..09b8904 100644 --- a/em2rp/lib/views/widgets/event_form/event_basic_info_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_basic_info_section.dart @@ -11,6 +11,7 @@ class EventBasicInfoSection extends StatelessWidget { final String? selectedEventTypeId; final DateTime? startDateTime; final DateTime? endDateTime; + final List> selectedOptions; final Function(String?) onEventTypeChanged; final Function(DateTime?) onStartDateTimeChanged; final Function(DateTime?) onEndDateTimeChanged; @@ -25,6 +26,7 @@ class EventBasicInfoSection extends StatelessWidget { required this.selectedEventTypeId, required this.startDateTime, required this.endDateTime, + required this.selectedOptions, required this.onEventTypeChanged, required this.onStartDateTimeChanged, required this.onEndDateTimeChanged, @@ -36,7 +38,6 @@ class EventBasicInfoSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('Informations principales'), TextFormField( controller: nameController, decoration: const InputDecoration( @@ -50,113 +51,104 @@ class EventBasicInfoSection extends StatelessWidget { if (isLoadingEventTypes) const Center(child: CircularProgressIndicator()) else - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - fit: FlexFit.loose, - child: DropdownButtonFormField( - initialValue: 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(width: 16), - Flexible( - fit: FlexFit.loose, - child: _buildDateTimeRow(context), - ), - ], + DropdownButtonFormField( + initialValue: 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), PriceHtTtcFields( basePriceController: basePriceController, onPriceChanged: onAnyFieldChanged, + selectedOptions: selectedOptions, ), ], ); } - 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, - ), - ), + final startField = GestureDetector( + onTap: () => _selectStartDateTime(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + decoration: const InputDecoration( + labelText: 'Début*', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.edit_calendar), ), - ), - 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, - ), - ), + controller: TextEditingController( + text: startDateTime == null + ? '' + : DateFormat('dd/MM/yyyy HH:mm').format(startDateTime!), ), + validator: (v) => startDateTime == null ? 'Champ requis' : null, ), - ], + ), + ); + + final endField = GestureDetector( + onTap: startDateTime == null ? null : () => _selectEndDateTime(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + decoration: const InputDecoration( + labelText: 'Fin*', + border: OutlineInputBorder(), + 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, + ), + ), + ); + + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 500) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: startField), + const SizedBox(width: 16), + Expanded(child: endField), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + startField, + const SizedBox(height: 16), + endField, + ], + ); + } + }, ); } diff --git a/em2rp/lib/views/widgets/event_form/event_details_section.dart b/em2rp/lib/views/widgets/event_form/event_details_section.dart index 7d3a985..42851cc 100644 --- a/em2rp/lib/views/widgets/event_form/event_details_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_details_section.dart @@ -56,8 +56,6 @@ class _EventDetailsSectionState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('Détails'), - // Description et champs de contact widget.isMobile ? _buildMobileLayout() @@ -87,8 +85,9 @@ class _EventDetailsSectionState extends State { ), ], ), + + const SizedBox(height: 20), - _buildSectionTitle('Adresse*'), TextFormField( controller: widget.addressController, decoration: const InputDecoration( @@ -196,17 +195,4 @@ class _EventDetailsSectionState extends State { ], ); } - - 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 index f82f5fc..adc38f4 100644 --- a/em2rp/lib/views/widgets/event_form/event_form_actions.dart +++ b/em2rp/lib/views/widgets/event_form/event_form_actions.dart @@ -20,67 +20,64 @@ class EventFormActions extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Bouton de suppression en mode édition - if (isEditMode && onDelete != null) - ElevatedButton.icon( - icon: const Icon(Icons.delete, color: Colors.white), - label: const Text('Supprimer'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - onPressed: isLoading ? null : onDelete, - ) - else - const SizedBox.shrink(), // Espace vide si pas en mode édition - - // Boutons Annuler et Enregistrer/Créer - Row( - mainAxisSize: MainAxisSize.min, - 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, + return Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, + runSpacing: 16, + children: [ + // Bouton de suppression en mode édition + if (isEditMode && onDelete != null) + ElevatedButton.icon( + icon: const Icon(Icons.delete, color: Colors.white), + label: const Text('Supprimer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, ), - ), + onPressed: isLoading ? null : onDelete, + ) + else + const SizedBox.shrink(), + + // Boutons Annuler, Enregistrer/Créer et Valider + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.end, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + TextButton( + onPressed: isLoading ? null : onCancel, + child: const Text('Annuler'), + ), + 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) + ElevatedButton.icon( + icon: const Icon(Icons.check_circle, color: Colors.white), + label: const Text('Créer et valider'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + onPressed: isLoading ? null : 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 index 852d329..c843916 100644 --- 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 @@ -36,16 +36,22 @@ class EventStaffAndDocumentsSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('Personnel'), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: UserMultiSelectWidget( - allUsers: allUsers, - selectedUserIds: selectedUserIds, - onChanged: onUserSelectionChanged, - isLoading: isLoadingUsers, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Personnel', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 8), + UserMultiSelectWidget( + allUsers: allUsers, + selectedUserIds: selectedUserIds, + onChanged: onUserSelectionChanged, + isLoading: isLoadingUsers, + ), + ], ), ), const SizedBox(width: 16), @@ -53,7 +59,8 @@ class EventStaffAndDocumentsSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('Documents'), + const Text('Documents', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 8), if (isMobile) _buildMobileDocumentUpload() else @@ -73,19 +80,6 @@ class EventStaffAndDocumentsSection extends StatelessWidget { ); } - 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, diff --git a/em2rp/lib/views/widgets/event_form/price_ht_ttc_fields.dart b/em2rp/lib/views/widgets/event_form/price_ht_ttc_fields.dart index b997a7e6..5e27450 100644 --- a/em2rp/lib/views/widgets/event_form/price_ht_ttc_fields.dart +++ b/em2rp/lib/views/widgets/event_form/price_ht_ttc_fields.dart @@ -8,12 +8,14 @@ class PriceHtTtcFields extends StatefulWidget { final TextEditingController basePriceController; final VoidCallback onPriceChanged; final double taxRate; + final List> selectedOptions; const PriceHtTtcFields({ super.key, required this.basePriceController, required this.onPriceChanged, this.taxRate = 0.20, + this.selectedOptions = const [], }); @override @@ -133,7 +135,7 @@ class _PriceHtTtcFieldsState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Prix', + 'Prix Base', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), @@ -205,7 +207,7 @@ class _PriceHtTtcFieldsState extends State { ], ), const SizedBox(height: 4), - // Affichage du montant de TVA (en plus petit) + // Affichage du montant de TVA (en plus petit) et du Prix Total Builder( builder: (context) { final htText = _htController.text.replaceAll(',', '.'); @@ -213,16 +215,72 @@ class _PriceHtTtcFieldsState extends State { if (htValue != null) { final taxAmount = PriceHelpers.calculateTax(htValue, taxRate: widget.taxRate); - return Padding( - padding: const EdgeInsets.only(left: 8.0, top: 4.0), - child: Text( - 'TVA (${(widget.taxRate * 100).toStringAsFixed(0)}%) : ${taxAmount.toStringAsFixed(2)} €', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - fontStyle: FontStyle.italic, + final ttcValue = PriceHelpers.calculateTTC(htValue, taxRate: widget.taxRate); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, top: 4.0), + child: Text( + 'TVA (${(widget.taxRate * 100).toStringAsFixed(0)}%) : ${taxAmount.toStringAsFixed(2)} €', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), ), - ), + if (widget.selectedOptions.isNotEmpty) ...[ + const SizedBox(height: 16), + Builder( + builder: (context) { + double optionsTotalTTC = 0.0; + for (var opt in widget.selectedOptions) { + // Les options sont stockées en TTC ou HT ? + // Généralement l'application utilise TTC. + final optPrice = opt['price'] is double ? opt['price'] : double.tryParse(opt['price'].toString()) ?? 0.0; + final optQuantity = opt['quantity'] is int ? opt['quantity'] : int.tryParse(opt['quantity'].toString()) ?? 1; + optionsTotalTTC += optPrice * optQuantity; + } + + final totalTTC = ttcValue + optionsTotalTTC; + final totalHT = PriceHelpers.calculateHT(totalTTC, taxRate: widget.taxRate); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade100), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Prix Total (Base + Options)', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${totalHT.toStringAsFixed(2)} € HT', + style: TextStyle(fontSize: 14, color: Colors.blue.shade800), + ), + Text( + '${totalTTC.toStringAsFixed(2)} € TTC', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade900), + ), + ], + ), + ], + ), + ); + } + ), + ], + ], ); } return const SizedBox.shrink();