Modif de l'affichage des données d'un événement et de l'afichage de la création/édition
Options sont maintenant géres dans firebase
This commit is contained in:
@@ -3,16 +3,16 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/option_model.dart';
|
||||
|
||||
class OptionSelectorWidget extends StatefulWidget {
|
||||
final String? eventType;
|
||||
final List<Map<String, dynamic>> selectedOptions;
|
||||
final ValueChanged<List<Map<String, dynamic>>> onChanged;
|
||||
final void Function(String name)? onRemove;
|
||||
final void Function(String id)? onRemove; // Changé de 'name' à 'id'
|
||||
final bool eventTypeRequired;
|
||||
final bool isMobile;
|
||||
final String? eventType;
|
||||
|
||||
const OptionSelectorWidget({
|
||||
super.key,
|
||||
required this.eventType,
|
||||
Key? key,
|
||||
this.eventType,
|
||||
required this.selectedOptions,
|
||||
required this.onChanged,
|
||||
this.onRemove,
|
||||
@@ -27,15 +27,11 @@ class OptionSelectorWidget extends StatefulWidget {
|
||||
class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
List<EventOption> _allOptions = [];
|
||||
bool _loading = true;
|
||||
final String _search = '';
|
||||
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
||||
int _rebuildKey = 0; // Clé pour forcer la reconstruction du FutureBuilder
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant OptionSelectorWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.eventType != widget.eventType) {
|
||||
_fetchOptions();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -62,7 +58,7 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
context: context,
|
||||
builder: (ctx) => _OptionPickerDialog(
|
||||
allOptions: _allOptions,
|
||||
eventType: widget.eventType,
|
||||
eventType: widget.eventType, // Ajout du paramètre manquant
|
||||
),
|
||||
);
|
||||
if (selected != null) {
|
||||
@@ -81,61 +77,104 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
Text('Options sélectionnées',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
children: widget.selectedOptions
|
||||
.map((opt) => Card(
|
||||
elevation: widget.isMobile ? 0 : 2,
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: widget.isMobile ? 4 : 8,
|
||||
horizontal: widget.isMobile ? 0 : 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(widget.isMobile ? 8 : 12)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(widget.isMobile ? 8.0 : 12.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(opt['name'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
if (opt['details'] != null &&
|
||||
opt['details'] != '')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Text(opt['details'],
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
Text('Prix : ${opt['price'] ?? ''} €',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: 'Supprimer cette option',
|
||||
onPressed: () {
|
||||
if (widget.onRemove != null) {
|
||||
widget.onRemove!(opt['name'] as String);
|
||||
} else {
|
||||
final newList = List<Map<String, dynamic>>.from(
|
||||
widget.selectedOptions)
|
||||
..removeWhere(
|
||||
(o) => o['name'] == opt['name']);
|
||||
widget.onChanged(newList);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
// Affichage direct des options sélectionnées avec clé de reconstruction
|
||||
widget.selectedOptions.isEmpty
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text('Aucune option sélectionnée'),
|
||||
)
|
||||
: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
key: ValueKey(_rebuildKey), // Clé pour forcer la reconstruction
|
||||
future: _loadOptionsWithDetails(widget.selectedOptions),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Erreur lors du chargement des options: ${snapshot.error}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final enrichedOptions = snapshot.data ?? [];
|
||||
|
||||
return Column(
|
||||
children: enrichedOptions
|
||||
.map((opt) => Card(
|
||||
elevation: widget.isMobile ? 0 : 2,
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: widget.isMobile ? 4 : 8,
|
||||
horizontal: widget.isMobile ? 0 : 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(widget.isMobile ? 8 : 12)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(widget.isMobile ? 8.0 : 12.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(opt['name'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
if (opt['details'] != null &&
|
||||
opt['details'].toString().trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Text(opt['details'],
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
Text('Prix : ${opt['price'] ?? ''} €',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: 'Supprimer cette option',
|
||||
onPressed: () {
|
||||
final optionId = opt['id'];
|
||||
|
||||
// Utiliser le callback onRemove si disponible
|
||||
if (widget.onRemove != null && optionId != null) {
|
||||
widget.onRemove!(optionId);
|
||||
// Forcer la reconstruction du FutureBuilder
|
||||
setState(() {
|
||||
_rebuildKey++;
|
||||
});
|
||||
} else {
|
||||
// Sinon, supprimer directement par ID de la liste
|
||||
final newList = List<Map<String, dynamic>>.from(widget.selectedOptions);
|
||||
newList.removeWhere((o) => o['id'] == optionId);
|
||||
widget.onChanged(newList);
|
||||
// Forcer la reconstruction du FutureBuilder
|
||||
setState(() {
|
||||
_rebuildKey++;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
@@ -148,32 +187,105 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Méthode pour charger les détails des options depuis Firebase
|
||||
Future<List<Map<String, dynamic>>> _loadOptionsWithDetails(List<Map<String, dynamic>> optionsData) async {
|
||||
List<Map<String, dynamic>> enrichedOptions = [];
|
||||
|
||||
for (final optionData in optionsData) {
|
||||
try {
|
||||
// Si l'option a un ID, récupérer les détails depuis Firestore
|
||||
if (optionData['id'] != null) {
|
||||
final doc = await FirebaseFirestore.instance
|
||||
.collection('options')
|
||||
.doc(optionData['id'])
|
||||
.get();
|
||||
|
||||
if (doc.exists) {
|
||||
final firestoreData = doc.data()!;
|
||||
enrichedOptions.add({
|
||||
'id': optionData['id'],
|
||||
'name': firestoreData['name'],
|
||||
'details': firestoreData['details'] ?? '',
|
||||
'price': optionData['price'],
|
||||
});
|
||||
} else {
|
||||
enrichedOptions.add({
|
||||
'id': optionData['id'],
|
||||
'name': 'Option supprimée',
|
||||
'details': 'Cette option n\'existe plus',
|
||||
'price': optionData['price'],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Ancien format, utiliser les données locales
|
||||
enrichedOptions.add(optionData);
|
||||
}
|
||||
} catch (e) {
|
||||
// En cas d'erreur, utiliser les données disponibles
|
||||
enrichedOptions.add({
|
||||
'id': optionData['id'],
|
||||
'name': optionData['name'] ?? 'Erreur de chargement',
|
||||
'details': 'Impossible de charger les détails',
|
||||
'price': optionData['price'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return enrichedOptions;
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionPickerDialog extends StatefulWidget {
|
||||
final List<EventOption> allOptions;
|
||||
final String? eventType;
|
||||
const _OptionPickerDialog(
|
||||
{required this.allOptions, required this.eventType});
|
||||
|
||||
const _OptionPickerDialog({
|
||||
required this.allOptions,
|
||||
this.eventType,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_OptionPickerDialog> createState() => _OptionPickerDialogState();
|
||||
}
|
||||
|
||||
|
||||
class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
||||
String _search = '';
|
||||
bool _creating = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Debug: Afficher les informations de filtrage
|
||||
print('=== DEBUG OptionPickerDialog ===');
|
||||
print('widget.eventType: ${widget.eventType}');
|
||||
print('widget.allOptions.length: ${widget.allOptions.length}');
|
||||
|
||||
final filtered = widget.allOptions.where((opt) {
|
||||
if (widget.eventType == null) return false;
|
||||
final matchesType =
|
||||
opt.eventTypes.any((ref) => ref.id == widget.eventType);
|
||||
final matchesSearch =
|
||||
opt.name.toLowerCase().contains(_search.toLowerCase());
|
||||
return matchesType && matchesSearch;
|
||||
print('Option: ${opt.name}');
|
||||
print(' opt.eventTypes: ${opt.eventTypes}');
|
||||
print(' widget.eventType: ${widget.eventType}');
|
||||
|
||||
if (widget.eventType == null) {
|
||||
print(' -> Filtered out: eventType is null');
|
||||
return false;
|
||||
}
|
||||
|
||||
final matchesType = opt.eventTypes.contains(widget.eventType);
|
||||
print(' -> matchesType: $matchesType');
|
||||
|
||||
final matchesSearch = opt.name.toLowerCase().contains(_search.toLowerCase());
|
||||
print(' -> matchesSearch: $matchesSearch');
|
||||
|
||||
final result = matchesType && matchesSearch;
|
||||
print(' -> Final result: $result');
|
||||
|
||||
return result;
|
||||
}).toList();
|
||||
|
||||
print('Filtered options count: ${filtered.length}');
|
||||
print('===========================');
|
||||
|
||||
return Dialog(
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
@@ -243,11 +355,8 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
||||
);
|
||||
if (price != null) {
|
||||
Navigator.pop(context, {
|
||||
'name': opt.name,
|
||||
'price': price,
|
||||
'compatibleTypes': opt.eventTypes
|
||||
.map((ref) => ref.id)
|
||||
.toList(),
|
||||
'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é)
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -304,9 +413,10 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
final _minPriceController = TextEditingController();
|
||||
final _maxPriceController = TextEditingController();
|
||||
final List<String> _selectedTypes = [];
|
||||
final List<String> _allTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
||||
String? _error;
|
||||
bool _checkingName = false;
|
||||
List<Map<String,dynamic>> _allEventTypes = [];
|
||||
bool _loading = true;
|
||||
|
||||
Future<bool> _isNameUnique(String name) async {
|
||||
final snap = await FirebaseFirestore.instance
|
||||
@@ -316,6 +426,23 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
return snap.docs.isEmpty;
|
||||
}
|
||||
|
||||
Future<void> _fetchEventTypes() async {
|
||||
setState(() {
|
||||
_loading=true;
|
||||
});
|
||||
final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get();
|
||||
setState(() {
|
||||
_allEventTypes = snapshot.docs.map((doc) => {'id': doc.id, 'name': doc['name']}).toList();
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchEventTypes();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
@@ -374,21 +501,21 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
const Text('Types d\'événement associés :'),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: _allTypes
|
||||
.map((type) => FilterChip(
|
||||
label: Text(type),
|
||||
selected: _selectedTypes.contains(type),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTypes.add(type);
|
||||
} else {
|
||||
_selectedTypes.remove(type);
|
||||
}
|
||||
});
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
children: _allEventTypes
|
||||
.map((type) => FilterChip(
|
||||
label: Text(type['name']),
|
||||
selected: _selectedTypes.contains(type['id']),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTypes.add(type['id']);
|
||||
} else {
|
||||
_selectedTypes.remove(type['id']);
|
||||
}
|
||||
});
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -435,22 +562,19 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
() => _error = 'Ce nom d\'option est déjà utilisé.');
|
||||
return;
|
||||
}
|
||||
final eventTypeRefs = _selectedTypes
|
||||
.map((type) => FirebaseFirestore.instance
|
||||
.collection('eventTypes')
|
||||
.doc(type))
|
||||
.toList();
|
||||
try {
|
||||
// Debug : afficher le contenu envoyé
|
||||
print('Enregistrement option avec eventTypes : [32m[1m[4m[7m' + _selectedTypes.toString() + '\u001b[0m');
|
||||
await FirebaseFirestore.instance.collection('options').add({
|
||||
'name': name,
|
||||
'details': _detailsController.text.trim(),
|
||||
'valMin': min,
|
||||
'valMax': max,
|
||||
'eventTypes': eventTypeRefs,
|
||||
'eventTypes': _selectedTypes,
|
||||
});
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
setState(() => _error = 'Erreur lors de la création : $e');
|
||||
setState(() => _error = 'Erreur lors de la création : ' + e.toString() + '\nEventTypes=' + _selectedTypes.toString());
|
||||
}
|
||||
},
|
||||
child: _checkingName
|
||||
|
||||
Reference in New Issue
Block a user