feat: Ajout de la gestion de la quantité pour les options d'événement
This commit is contained in:
@@ -6,6 +6,7 @@ class EventOption {
|
||||
final double valMin;
|
||||
final double valMax;
|
||||
final List<String> eventTypes; // Changé de List<DocumentReference> à List<String>
|
||||
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<String, dynamic> map, String id) {
|
||||
@@ -28,6 +30,7 @@ class EventOption {
|
||||
eventTypes: (map['eventTypes'] as List<dynamic>? ?? [])
|
||||
.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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EventDetailsHeader> createState() => _EventDetailsHeaderState();
|
||||
}
|
||||
|
||||
class _EventDetailsHeaderState extends State<EventDetailsHeader> {
|
||||
String? _eventTypeName;
|
||||
bool _isLoadingEventType = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchEventTypeName();
|
||||
}
|
||||
|
||||
Future<void> _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<LocalUserProvider>(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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -55,7 +55,11 @@ class EventDetailsInfo extends StatelessWidget {
|
||||
final total = event.basePrice +
|
||||
event.options.fold<num>(
|
||||
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),
|
||||
|
||||
@@ -392,6 +392,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
|
||||
final _minPriceController = TextEditingController();
|
||||
final _maxPriceController = TextEditingController();
|
||||
List<String> _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: [
|
||||
|
||||
@@ -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<Map<String, dynamic>> options, NumberFormat currencyFormat) {
|
||||
final optionsTotal = options.fold<num>(0, (sum, opt) => sum + (opt['price'] ?? 0.0));
|
||||
final optionsTotal = options.fold<num>(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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,8 +149,65 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
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<Map<String, dynamic>>.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<Map<String, dynamic>>.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<OptionSelectorWidget> {
|
||||
: 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<OptionSelectorWidget> {
|
||||
'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<OptionSelectorWidget> {
|
||||
'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<double>(
|
||||
|
||||
final result = await showDialog<Map<String, dynamic>>(
|
||||
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<String> _selectedTypes = [];
|
||||
bool _isQuantitative = false;
|
||||
String? _error;
|
||||
bool _checkingCode = false;
|
||||
List<Map<String,dynamic>> _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) {
|
||||
|
||||
Reference in New Issue
Block a user