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 valMin;
|
||||||
final double valMax;
|
final double valMax;
|
||||||
final List<String> eventTypes; // Changé de List<DocumentReference> à List<String>
|
final List<String> eventTypes; // Changé de List<DocumentReference> à List<String>
|
||||||
|
final bool isQuantitative; // Indique si l'option peut avoir une quantité
|
||||||
|
|
||||||
EventOption({
|
EventOption({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -15,6 +16,7 @@ class EventOption {
|
|||||||
required this.valMin,
|
required this.valMin,
|
||||||
required this.valMax,
|
required this.valMax,
|
||||||
required this.eventTypes,
|
required this.eventTypes,
|
||||||
|
this.isQuantitative = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory EventOption.fromMap(Map<String, dynamic> map, String id) {
|
factory EventOption.fromMap(Map<String, dynamic> map, String id) {
|
||||||
@@ -28,6 +30,7 @@ class EventOption {
|
|||||||
eventTypes: (map['eventTypes'] as List<dynamic>? ?? [])
|
eventTypes: (map['eventTypes'] as List<dynamic>? ?? [])
|
||||||
.map((e) => e.toString()) // Convertit en String (supporte IDs et références)
|
.map((e) => e.toString()) // Convertit en String (supporte IDs et références)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
isQuantitative: map['isQuantitative'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ class EventOption {
|
|||||||
'valMin': valMin,
|
'valMin': valMin,
|
||||||
'valMax': valMax,
|
'valMax': valMax,
|
||||||
'eventTypes': eventTypes,
|
'eventTypes': eventTypes,
|
||||||
|
'isQuantitative': isQuantitative,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/views/event_add_page.dart';
|
import 'package:em2rp/views/event_add_page.dart';
|
||||||
|
|
||||||
class EventDetailsHeader extends StatelessWidget {
|
class EventDetailsHeader extends StatefulWidget {
|
||||||
final EventModel event;
|
final EventModel event;
|
||||||
|
|
||||||
const EventDetailsHeader({
|
const EventDetailsHeader({
|
||||||
@@ -13,6 +14,52 @@ class EventDetailsHeader extends StatelessWidget {
|
|||||||
required this.event,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
@@ -25,7 +72,7 @@ class EventDetailsHeader extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxHeight: 80),
|
constraints: const BoxConstraints(maxHeight: 80),
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
event.name,
|
widget.event.name,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
color: AppColors.noir,
|
color: AppColors.noir,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -34,7 +81,9 @@ class EventDetailsHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
event.eventTypeId,
|
_isLoadingEventType
|
||||||
|
? 'Chargement...'
|
||||||
|
: _eventTypeName ?? widget.event.eventTypeId,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: AppColors.rouge,
|
color: AppColors.rouge,
|
||||||
),
|
),
|
||||||
@@ -43,7 +92,7 @@ class EventDetailsHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
_buildStatusIcon(event.status),
|
_buildStatusIcon(widget.event.status),
|
||||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||||
.hasPermission('edit_event')) ...[
|
.hasPermission('edit_event')) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -53,7 +102,7 @@ class EventDetailsHeader extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => EventAddEditPage(event: event),
|
builder: (context) => EventAddEditPage(event: widget.event),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ class EventDetailsInfo extends StatelessWidget {
|
|||||||
final total = event.basePrice +
|
final total = event.basePrice +
|
||||||
event.options.fold<num>(
|
event.options.fold<num>(
|
||||||
0,
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
|
|||||||
final _minPriceController = TextEditingController();
|
final _minPriceController = TextEditingController();
|
||||||
final _maxPriceController = TextEditingController();
|
final _maxPriceController = TextEditingController();
|
||||||
List<String> _selectedTypes = [];
|
List<String> _selectedTypes = [];
|
||||||
|
bool _isQuantitative = false;
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
|
||||||
@@ -405,6 +406,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
|
|||||||
_minPriceController.text = widget.option!.valMin.toString();
|
_minPriceController.text = widget.option!.valMin.toString();
|
||||||
_maxPriceController.text = widget.option!.valMax.toString();
|
_maxPriceController.text = widget.option!.valMax.toString();
|
||||||
_selectedTypes = List.from(widget.option!.eventTypes);
|
_selectedTypes = List.from(widget.option!.eventTypes);
|
||||||
|
_isQuantitative = widget.option!.isQuantitative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +478,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
|
|||||||
'valMin': min,
|
'valMin': min,
|
||||||
'valMax': max,
|
'valMax': max,
|
||||||
'eventTypes': _selectedTypes,
|
'eventTypes': _selectedTypes,
|
||||||
|
'isQuantitative': _isQuantitative,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (widget.option == null) {
|
if (widget.option == null) {
|
||||||
@@ -584,6 +587,18 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
...enrichedOptions.map((opt) {
|
...enrichedOptions.map((opt) {
|
||||||
final price = (opt['price'] ?? 0.0) as num;
|
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(
|
return ListTile(
|
||||||
leading: Icon(Icons.tune, color: AppColors.rouge),
|
leading: Icon(Icons.tune, color: AppColors.rouge),
|
||||||
@@ -72,8 +74,11 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
: opt['name'] ?? '',
|
: opt['name'] ?? '',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: opt['details'] != null && opt['details'].toString().trim().isNotEmpty
|
subtitle: Column(
|
||||||
? Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (opt['details'] != null && opt['details'].toString().trim().isNotEmpty)
|
||||||
|
Text(
|
||||||
opt['details'].toString().trim(),
|
opt['details'].toString().trim(),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
@@ -82,7 +87,8 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
else
|
||||||
|
Text(
|
||||||
'Aucun détail disponible',
|
'Aucun détail disponible',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
@@ -91,10 +97,24 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
fontStyle: FontStyle.italic,
|
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
|
trailing: canViewPrices
|
||||||
? Text(
|
? Text(
|
||||||
(isNegative ? '- ' : '+ ') +
|
(isNegative ? '- ' : '+ ') +
|
||||||
currencyFormat.format(price.abs()),
|
currencyFormat.format(totalPrice.abs()),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isNegative ? Colors.red : AppColors.noir,
|
color: isNegative ? Colors.red : AppColors.noir,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -118,7 +138,11 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTotalPrice(BuildContext context, List<Map<String, dynamic>> options, NumberFormat currencyFormat) {
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
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
|
'name': firestoreData['name'], // Récupéré depuis Firestore
|
||||||
'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore
|
'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore
|
||||||
'price': optionData['price'], // Prix choisi par l'utilisateur
|
'price': optionData['price'], // Prix choisi par l'utilisateur
|
||||||
|
'quantity': optionData['quantity'] ?? 1, // Quantité
|
||||||
|
'isQuantitative': firestoreData['isQuantitative'] ?? false,
|
||||||
'valMin': firestoreData['valMin'],
|
'valMin': firestoreData['valMin'],
|
||||||
'valMax': firestoreData['valMax'],
|
'valMax': firestoreData['valMax'],
|
||||||
});
|
});
|
||||||
@@ -176,6 +202,8 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
'name': 'Option supprimée (ID: ${optionData['id']})',
|
'name': 'Option supprimée (ID: ${optionData['id']})',
|
||||||
'details': 'Cette option n\'existe plus dans la base de données',
|
'details': 'Cette option n\'existe plus dans la base de données',
|
||||||
'price': optionData['price'],
|
'price': optionData['price'],
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -185,6 +213,8 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
'name': optionData['name'] ?? 'Option inconnue',
|
'name': optionData['name'] ?? 'Option inconnue',
|
||||||
'details': optionData['details'] ?? 'Aucun détail disponible',
|
'details': optionData['details'] ?? 'Aucun détail disponible',
|
||||||
'price': optionData['price'] ?? 0.0,
|
'price': optionData['price'] ?? 0.0,
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -195,6 +225,8 @@ class EventOptionsDisplayWidget extends StatelessWidget {
|
|||||||
'name': 'Erreur de chargement (ID: ${optionData['id']})',
|
'name': 'Erreur de chargement (ID: ${optionData['id']})',
|
||||||
'details': 'Impossible de charger les détails de cette option',
|
'details': 'Impossible de charger les détails de cette option',
|
||||||
'price': optionData['price'] ?? 0.0,
|
'price': optionData['price'] ?? 0.0,
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,8 +149,65 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
|||||||
child: Text(opt['details'],
|
child: Text(opt['details'],
|
||||||
style: const TextStyle(fontSize: 13)),
|
style: const TextStyle(fontSize: 13)),
|
||||||
),
|
),
|
||||||
Text('Prix : ${opt['price'] ?? ''} €',
|
Row(
|
||||||
style: const TextStyle(fontSize: 13)),
|
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
|
: firestoreData['name'], // Affichage avec code
|
||||||
'details': firestoreData['details'] ?? '',
|
'details': firestoreData['details'] ?? '',
|
||||||
'price': optionData['price'],
|
'price': optionData['price'],
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': firestoreData['isQuantitative'] ?? false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
enrichedOptions.add({
|
enrichedOptions.add({
|
||||||
@@ -231,11 +290,17 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
|||||||
'name': 'Option supprimée (${optionData['id']})',
|
'name': 'Option supprimée (${optionData['id']})',
|
||||||
'details': 'Cette option n\'existe plus',
|
'details': 'Cette option n\'existe plus',
|
||||||
'price': optionData['price'],
|
'price': optionData['price'],
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Ancien format, utiliser les données locales
|
// Ancien format, utiliser les données locales
|
||||||
enrichedOptions.add(optionData);
|
enrichedOptions.add({
|
||||||
|
...optionData,
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': optionData['isQuantitative'] ?? false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// En cas d'erreur, utiliser les données disponibles
|
// En cas d'erreur, utiliser les données disponibles
|
||||||
@@ -244,6 +309,8 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
|||||||
'name': optionData['name'] ?? 'Erreur de chargement',
|
'name': optionData['name'] ?? 'Erreur de chargement',
|
||||||
'details': 'Impossible de charger les détails',
|
'details': 'Impossible de charger les détails',
|
||||||
'price': optionData['price'],
|
'price': optionData['price'],
|
||||||
|
'quantity': optionData['quantity'] ?? 1,
|
||||||
|
'isQuantitative': false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,27 +421,46 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
|||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final opt = filtered[i];
|
final opt = filtered[i];
|
||||||
return ListTile(
|
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}€'),
|
subtitle: Text('${opt.details}\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final min = opt.valMin;
|
final min = opt.valMin;
|
||||||
final max = opt.valMax;
|
final max = opt.valMax;
|
||||||
final defaultPrice =
|
final defaultPrice =
|
||||||
((min + max) / 2).toStringAsFixed(2);
|
((min + max) / 2).toStringAsFixed(2);
|
||||||
final price = await showDialog<double>(
|
|
||||||
|
final result = await showDialog<Map<String, dynamic>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) {
|
builder: (ctx) {
|
||||||
final priceController =
|
final priceController =
|
||||||
TextEditingController(text: defaultPrice);
|
TextEditingController(text: defaultPrice);
|
||||||
|
final quantityController =
|
||||||
|
TextEditingController(text: '1');
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Prix pour ${opt.code} - ${opt.name}'), // Affichage avec code
|
title: Text('Ajouter ${opt.code} - ${opt.name}'), // Affichage avec code
|
||||||
content: TextField(
|
content: Column(
|
||||||
controller: priceController,
|
mainAxisSize: MainAxisSize.min,
|
||||||
keyboardType:
|
children: [
|
||||||
const TextInputType.numberWithOptions(
|
TextField(
|
||||||
decimal: true),
|
controller: priceController,
|
||||||
decoration: const InputDecoration(
|
keyboardType:
|
||||||
labelText: 'Prix (€)'),
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -387,7 +473,13 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
|||||||
priceController.text
|
priceController.text
|
||||||
.replaceAll(',', '.')) ??
|
.replaceAll(',', '.')) ??
|
||||||
0.0;
|
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'),
|
child: const Text('Ajouter'),
|
||||||
),
|
),
|
||||||
@@ -395,10 +487,11 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (price != null) {
|
if (result != null) {
|
||||||
Navigator.pop(context, {
|
Navigator.pop(context, {
|
||||||
'id': opt.id, // ID de l'option (obligatoire pour récupérer les données)
|
'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 _minPriceController = TextEditingController();
|
||||||
final _maxPriceController = TextEditingController();
|
final _maxPriceController = TextEditingController();
|
||||||
final List<String> _selectedTypes = [];
|
final List<String> _selectedTypes = [];
|
||||||
|
bool _isQuantitative = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
bool _checkingCode = false;
|
bool _checkingCode = false;
|
||||||
List<Map<String,dynamic>> _allEventTypes = [];
|
List<Map<String,dynamic>> _allEventTypes = [];
|
||||||
@@ -559,6 +653,19 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -642,6 +749,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
|||||||
'valMin': min,
|
'valMin': min,
|
||||||
'valMax': max,
|
'valMax': max,
|
||||||
'eventTypes': _selectedTypes,
|
'eventTypes': _selectedTypes,
|
||||||
|
'isQuantitative': _isQuantitative,
|
||||||
});
|
});
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user