diff --git a/em2rp/lib/models/option_model.dart b/em2rp/lib/models/option_model.dart index 8745537..5b898b7 100644 --- a/em2rp/lib/models/option_model.dart +++ b/em2rp/lib/models/option_model.dart @@ -6,6 +6,7 @@ class EventOption { final double valMin; final double valMax; final List eventTypes; // Changé de List à List + final bool isQuantitative; // Indique si l'option peut avoir une quantité EventOption({ required this.id, @@ -15,6 +16,7 @@ class EventOption { required this.valMin, required this.valMax, required this.eventTypes, + this.isQuantitative = false, }); factory EventOption.fromMap(Map map, String id) { @@ -28,6 +30,7 @@ class EventOption { eventTypes: (map['eventTypes'] as List? ?? []) .map((e) => e.toString()) // Convertit en String (supporte IDs et références) .toList(), + isQuantitative: map['isQuantitative'] ?? false, ); } @@ -39,6 +42,7 @@ class EventOption { 'valMin': valMin, 'valMax': valMax, 'eventTypes': eventTypes, + 'isQuantitative': isQuantitative, }; } } diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart index e55ada5..e821ecd 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_header.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/views/event_add_page.dart'; -class EventDetailsHeader extends StatelessWidget { +class EventDetailsHeader extends StatefulWidget { final EventModel event; const EventDetailsHeader({ @@ -13,6 +14,52 @@ class EventDetailsHeader extends StatelessWidget { required this.event, }); + @override + State createState() => _EventDetailsHeaderState(); +} + +class _EventDetailsHeaderState extends State { + String? _eventTypeName; + bool _isLoadingEventType = true; + + @override + void initState() { + super.initState(); + _fetchEventTypeName(); + } + + Future _fetchEventTypeName() async { + try { + if (widget.event.eventTypeId.isEmpty) { + setState(() => _isLoadingEventType = false); + return; + } + + final doc = await FirebaseFirestore.instance + .collection('eventTypes') + .doc(widget.event.eventTypeId) + .get(); + + if (doc.exists) { + setState(() { + _eventTypeName = doc.data()?['name'] as String? ?? widget.event.eventTypeId; + _isLoadingEventType = false; + }); + } else { + setState(() { + _eventTypeName = widget.event.eventTypeId; + _isLoadingEventType = false; + }); + } + } catch (e) { + print('Erreur lors du chargement du type d\'événement: $e'); + setState(() { + _eventTypeName = widget.event.eventTypeId; + _isLoadingEventType = false; + }); + } + } + @override Widget build(BuildContext context) { return Row( @@ -25,7 +72,7 @@ class EventDetailsHeader extends StatelessWidget { Container( constraints: const BoxConstraints(maxHeight: 80), child: SelectableText( - event.name, + widget.event.name, style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: AppColors.noir, fontWeight: FontWeight.bold, @@ -34,7 +81,9 @@ class EventDetailsHeader extends StatelessWidget { ), const SizedBox(height: 4), Text( - event.eventTypeId, + _isLoadingEventType + ? 'Chargement...' + : _eventTypeName ?? widget.event.eventTypeId, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.rouge, ), @@ -43,7 +92,7 @@ class EventDetailsHeader extends StatelessWidget { ), ), const SizedBox(width: 12), - _buildStatusIcon(event.status), + _buildStatusIcon(widget.event.status), if (Provider.of(context, listen: false) .hasPermission('edit_event')) ...[ const SizedBox(width: 8), @@ -53,7 +102,7 @@ class EventDetailsHeader extends StatelessWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => EventAddEditPage(event: event), + builder: (context) => EventAddEditPage(event: widget.event), ), ); }, diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart index 247d256..5743409 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_details_info.dart @@ -55,7 +55,11 @@ class EventDetailsInfo extends StatelessWidget { final total = event.basePrice + event.options.fold( 0, - (sum, opt) => sum + (opt['price'] ?? 0.0), + (sum, opt) { + final price = opt['price'] ?? 0.0; + final quantity = opt['quantity'] ?? 1; + return sum + (price * quantity); + }, ); return Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), diff --git a/em2rp/lib/views/widgets/data_management/options_management.dart b/em2rp/lib/views/widgets/data_management/options_management.dart index 210aaf1..4adbec8 100644 --- a/em2rp/lib/views/widgets/data_management/options_management.dart +++ b/em2rp/lib/views/widgets/data_management/options_management.dart @@ -392,6 +392,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { final _minPriceController = TextEditingController(); final _maxPriceController = TextEditingController(); List _selectedTypes = []; + bool _isQuantitative = false; bool _loading = false; String? _error; @@ -405,6 +406,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { _minPriceController.text = widget.option!.valMin.toString(); _maxPriceController.text = widget.option!.valMax.toString(); _selectedTypes = List.from(widget.option!.eventTypes); + _isQuantitative = widget.option!.isQuantitative; } } @@ -476,6 +478,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { 'valMin': min, 'valMax': max, 'eventTypes': _selectedTypes, + 'isQuantitative': _isQuantitative, }; if (widget.option == null) { @@ -584,6 +587,18 @@ class _OptionFormDialogState extends State<_OptionFormDialog> { ], ), const SizedBox(height: 16), + CheckboxListTile( + title: const Text('Option quantitative'), + subtitle: const Text('Permet de spécifier une quantité lors de l\'ajout à un événement'), + value: _isQuantitative, + onChanged: (value) { + setState(() { + _isQuantitative = value ?? false; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + ), + const SizedBox(height: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart b/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart index 2659b21..2fc9ecb 100644 --- a/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart +++ b/em2rp/lib/views/widgets/event_form/event_options_display_widget.dart @@ -62,7 +62,9 @@ class EventOptionsDisplayWidget extends StatelessWidget { children: [ ...enrichedOptions.map((opt) { final price = (opt['price'] ?? 0.0) as num; - final isNegative = price < 0; + final quantity = (opt['quantity'] ?? 1) as int; + final totalPrice = price * quantity; + final isNegative = totalPrice < 0; return ListTile( leading: Icon(Icons.tune, color: AppColors.rouge), @@ -72,8 +74,11 @@ class EventOptionsDisplayWidget extends StatelessWidget { : opt['name'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold), ), - subtitle: opt['details'] != null && opt['details'].toString().trim().isNotEmpty - ? Text( + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (opt['details'] != null && opt['details'].toString().trim().isNotEmpty) + Text( opt['details'].toString().trim(), style: const TextStyle( fontWeight: FontWeight.normal, @@ -82,7 +87,8 @@ class EventOptionsDisplayWidget extends StatelessWidget { fontStyle: FontStyle.italic, ), ) - : Text( + else + Text( 'Aucun détail disponible', style: TextStyle( fontWeight: FontWeight.normal, @@ -91,10 +97,24 @@ class EventOptionsDisplayWidget extends StatelessWidget { fontStyle: FontStyle.italic, ), ), + if (quantity > 1 && canViewPrices) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '${currencyFormat.format(price)} × $quantity', + style: TextStyle( + color: Colors.grey[700], + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), trailing: canViewPrices ? Text( (isNegative ? '- ' : '+ ') + - currencyFormat.format(price.abs()), + currencyFormat.format(totalPrice.abs()), style: TextStyle( color: isNegative ? Colors.red : AppColors.noir, fontWeight: FontWeight.bold, @@ -118,7 +138,11 @@ class EventOptionsDisplayWidget extends StatelessWidget { } Widget _buildTotalPrice(BuildContext context, List> options, NumberFormat currencyFormat) { - final optionsTotal = options.fold(0, (sum, opt) => sum + (opt['price'] ?? 0.0)); + final optionsTotal = options.fold(0, (sum, opt) { + final price = opt['price'] ?? 0.0; + final quantity = opt['quantity'] ?? 1; + return sum + (price * quantity); + }); return Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), @@ -166,6 +190,8 @@ class EventOptionsDisplayWidget extends StatelessWidget { 'name': firestoreData['name'], // Récupéré depuis Firestore 'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore 'price': optionData['price'], // Prix choisi par l'utilisateur + 'quantity': optionData['quantity'] ?? 1, // Quantité + 'isQuantitative': firestoreData['isQuantitative'] ?? false, 'valMin': firestoreData['valMin'], 'valMax': firestoreData['valMax'], }); @@ -176,6 +202,8 @@ class EventOptionsDisplayWidget extends StatelessWidget { 'name': 'Option supprimée (ID: ${optionData['id']})', 'details': 'Cette option n\'existe plus dans la base de données', 'price': optionData['price'], + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': false, }); } } else { @@ -185,6 +213,8 @@ class EventOptionsDisplayWidget extends StatelessWidget { 'name': optionData['name'] ?? 'Option inconnue', 'details': optionData['details'] ?? 'Aucun détail disponible', 'price': optionData['price'] ?? 0.0, + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': false, }); } } catch (e) { @@ -195,6 +225,8 @@ class EventOptionsDisplayWidget extends StatelessWidget { 'name': 'Erreur de chargement (ID: ${optionData['id']})', 'details': 'Impossible de charger les détails de cette option', 'price': optionData['price'] ?? 0.0, + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': false, }); } } diff --git a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart index f20aa9c..11526c4 100644 --- a/em2rp/lib/views/widgets/inputs/option_selector_widget.dart +++ b/em2rp/lib/views/widgets/inputs/option_selector_widget.dart @@ -149,8 +149,65 @@ class _OptionSelectorWidgetState extends State { child: Text(opt['details'], style: const TextStyle(fontSize: 13)), ), - Text('Prix : ${opt['price'] ?? ''} €', - style: const TextStyle(fontSize: 13)), + Row( + children: [ + Text('Prix unitaire : ${opt['price'] ?? ''} €', + style: const TextStyle(fontSize: 13)), + if (opt['isQuantitative'] == true && opt['quantity'] != null && opt['quantity'] > 1) ...[ + const Text(' × ', style: TextStyle(fontSize: 13)), + Text('${opt['quantity']}', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), + Text(' = ${((opt['price'] ?? 0) * (opt['quantity'] ?? 1)).toStringAsFixed(2)} €', + style: const TextStyle(fontSize: 13, color: Colors.green, fontWeight: FontWeight.bold)), + ], + ], + ), + if (opt['isQuantitative'] == true) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Row( + children: [ + const Text('Quantité : ', style: TextStyle(fontSize: 12)), + IconButton( + icon: const Icon(Icons.remove_circle_outline, size: 20), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + final currentQty = opt['quantity'] ?? 1; + if (currentQty > 1) { + final newList = List>.from(widget.selectedOptions); + final index = newList.indexWhere((o) => o['id'] == opt['id']); + if (index != -1) { + newList[index] = {...newList[index], 'quantity': currentQty - 1}; + widget.onChanged(newList); + setState(() => _rebuildKey++); + } + } + }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text('${opt['quantity'] ?? 1}', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + ), + IconButton( + icon: const Icon(Icons.add_circle_outline, size: 20), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + final currentQty = opt['quantity'] ?? 1; + final newList = List>.from(widget.selectedOptions); + final index = newList.indexWhere((o) => o['id'] == opt['id']); + if (index != -1) { + newList[index] = {...newList[index], 'quantity': currentQty + 1}; + widget.onChanged(newList); + setState(() => _rebuildKey++); + } + }, + ), + ], + ), + ), ], ), ), @@ -224,6 +281,8 @@ class _OptionSelectorWidgetState extends State { : firestoreData['name'], // Affichage avec code 'details': firestoreData['details'] ?? '', 'price': optionData['price'], + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': firestoreData['isQuantitative'] ?? false, }); } else { enrichedOptions.add({ @@ -231,11 +290,17 @@ class _OptionSelectorWidgetState extends State { 'name': 'Option supprimée (${optionData['id']})', 'details': 'Cette option n\'existe plus', 'price': optionData['price'], + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': false, }); } } else { // Ancien format, utiliser les données locales - enrichedOptions.add(optionData); + enrichedOptions.add({ + ...optionData, + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': optionData['isQuantitative'] ?? false, + }); } } catch (e) { // En cas d'erreur, utiliser les données disponibles @@ -244,6 +309,8 @@ class _OptionSelectorWidgetState extends State { 'name': optionData['name'] ?? 'Erreur de chargement', 'details': 'Impossible de charger les détails', 'price': optionData['price'], + 'quantity': optionData['quantity'] ?? 1, + 'isQuantitative': false, }); } } @@ -354,27 +421,46 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { itemBuilder: (context, i) { final opt = filtered[i]; return ListTile( - title: Text('${opt.code} - ${opt.name}'), // Affichage avec code + title: Text('${opt.code} - ${opt.name}${opt.isQuantitative ? ' (Quantitatif)' : ''}'), // Affichage avec code subtitle: Text('${opt.details}\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'), onTap: () async { final min = opt.valMin; final max = opt.valMax; final defaultPrice = ((min + max) / 2).toStringAsFixed(2); - final price = await showDialog( + + final result = await showDialog>( context: context, builder: (ctx) { final priceController = TextEditingController(text: defaultPrice); + final quantityController = + TextEditingController(text: '1'); return AlertDialog( - title: Text('Prix pour ${opt.code} - ${opt.name}'), // Affichage avec code - content: TextField( - controller: priceController, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true), - decoration: const InputDecoration( - labelText: 'Prix (€)'), + title: Text('Ajouter ${opt.code} - ${opt.name}'), // Affichage avec code + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: priceController, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true), + decoration: const InputDecoration( + labelText: 'Prix unitaire (€)'), + ), + if (opt.isQuantitative) ...[ + const SizedBox(height: 16), + TextField( + controller: quantityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Quantité', + helperText: 'Le prix sera multiplié par la quantité', + ), + ), + ], + ], ), actions: [ TextButton( @@ -387,7 +473,13 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { priceController.text .replaceAll(',', '.')) ?? 0.0; - Navigator.pop(ctx, price); + final quantity = opt.isQuantitative + ? (int.tryParse(quantityController.text) ?? 1) + : 1; + Navigator.pop(ctx, { + 'price': price, + 'quantity': quantity, + }); }, child: const Text('Ajouter'), ), @@ -395,10 +487,11 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { ); }, ); - if (price != null) { + if (result != null) { Navigator.pop(context, { 'id': opt.id, // ID de l'option (obligatoire pour récupérer les données) - 'price': price, // Prix choisi par l'utilisateur (obligatoire car personnalisé) + 'price': result['price'], // Prix choisi par l'utilisateur (obligatoire car personnalisé) + 'quantity': result['quantity'] ?? 1, // Quantité (par défaut 1) }); } }, @@ -457,6 +550,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { final _minPriceController = TextEditingController(); final _maxPriceController = TextEditingController(); final List _selectedTypes = []; + bool _isQuantitative = false; String? _error; bool _checkingCode = false; List> _allEventTypes = []; @@ -559,6 +653,19 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { ], ), const SizedBox(height: 8), + CheckboxListTile( + title: const Text('Option quantitative'), + subtitle: const Text('Permet de spécifier une quantité'), + value: _isQuantitative, + onChanged: (value) { + setState(() { + _isQuantitative = value ?? false; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + const SizedBox(height: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -642,6 +749,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> { 'valMin': min, 'valMax': max, 'eventTypes': _selectedTypes, + 'isQuantitative': _isQuantitative, }); Navigator.pop(context, true); } catch (e) {