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:
ElPoyo
2025-10-10 19:20:38 +02:00
parent aae68f8ab7
commit 4128ddc34a
13 changed files with 850 additions and 299 deletions

View File

@@ -1,10 +1,10 @@
class Env { class Env {
static const bool isDevelopment = false; static const bool isDevelopment = true;
// Configuration de l'auto-login en développement // Configuration de l'auto-login en développement
static const String devAdminEmail = 'paul.fournel@em2events.fr'; static const String devAdminEmail = 'paul.fournel@em2events.fr';
static const String devAdminPassword = static const String devAdminPassword =
"Azerty\$1!"; // À remplacer par le vrai mot de passe "Pastis51!"; // À remplacer par le vrai mot de passe
// URLs et endpoints // URLs et endpoints
static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com'; static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com';

View File

@@ -363,6 +363,39 @@ class EventFormController extends ChangeNotifier {
} }
} }
Future<bool> deleteEvent(BuildContext context, String eventId) async {
_isLoading = true;
_error = null;
_success = null;
notifyListeners();
try {
// Supprimer l'événement de Firestore
await FirebaseFirestore.instance.collection('events').doc(eventId).delete();
// Recharger la liste des événements
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final userId = localUserProvider.uid;
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
if (userId != null) {
await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
}
_success = "Événement supprimé avec succès !";
notifyListeners();
return true;
} catch (e) {
_error = "Erreur lors de la suppression : $e";
notifyListeners();
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
void clearError() { void clearError() {
_error = null; _error = null;
notifyListeners(); notifyListeners();

View File

@@ -54,7 +54,6 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print("test");
return MaterialApp( return MaterialApp(
title: 'EM2 ERP', title: 'EM2 ERP',
theme: ThemeData( theme: ThemeData(

View File

@@ -72,62 +72,129 @@ class EventModel {
}); });
factory EventModel.fromMap(Map<String, dynamic> map, String id) { factory EventModel.fromMap(Map<String, dynamic> map, String id) {
final List<dynamic> workforceRefs = map['workforce'] ?? []; try {
final Timestamp? startTimestamp = map['StartDateTime'] as Timestamp?; // Gestion sécurisée des références workforce
final Timestamp? endTimestamp = map['EndDateTime'] as Timestamp?; final List<dynamic> workforceRefs = map['workforce'] ?? [];
final List<DocumentReference> safeWorkforce = [];
final docsRaw = map['documents'] ?? []; for (var ref in workforceRefs) {
final docs = docsRaw is List if (ref is DocumentReference) {
? docsRaw.map<Map<String, String>>((e) { safeWorkforce.add(ref);
} else {
print('Warning: Invalid workforce reference in event $id: $ref');
}
}
// Gestion sécurisée des timestamps
final Timestamp? startTimestamp = map['StartDateTime'] as Timestamp?;
final Timestamp? endTimestamp = map['EndDateTime'] as Timestamp?;
final DateTime startDate = startTimestamp?.toDate() ?? DateTime.now();
final DateTime endDate = endTimestamp?.toDate() ??
startDate.add(const Duration(hours: 1));
// Gestion sécurisée des documents
final docsRaw = map['documents'] ?? [];
final List<Map<String, String>> docs = [];
if (docsRaw is List) {
for (var e in docsRaw) {
try {
if (e is Map) { if (e is Map) {
return Map<String, String>.from(e); docs.add(Map<String, String>.from(e));
} else if (e is String) { } else if (e is String) {
final fileName = Uri.decodeComponent( final fileName = Uri.decodeComponent(
e.split('/').last.split('?').first, e.split('/').last.split('?').first,
); );
return {'name': fileName, 'url': e}; docs.add({'name': fileName, 'url': e});
} else {
return {};
} }
}).toList() } catch (docError) {
: <Map<String, String>>[]; print('Warning: Failed to parse document in event $id: $docError');
final optionsRaw = map['options'] ?? []; }
final options = optionsRaw is List }
? optionsRaw.map<Map<String, dynamic>>((e) { }
// Gestion sécurisée des options
final optionsRaw = map['options'] ?? [];
final List<Map<String, dynamic>> options = [];
if (optionsRaw is List) {
for (var e in optionsRaw) {
try {
if (e is Map) { if (e is Map) {
return Map<String, dynamic>.from(e); options.add(Map<String, dynamic>.from(e));
} else {
return {};
} }
}).toList() } catch (optionError) {
: <Map<String, dynamic>>[]; print('Warning: Failed to parse option in event $id: $optionError');
return EventModel( }
id: id, }
name: map['Name'] ?? '', }
description: map['Description'] ?? '',
startDateTime: startTimestamp?.toDate() ?? DateTime.now(), // Gestion sécurisée de l'EventType
endDateTime: endTimestamp?.toDate() ?? String eventTypeId = '';
DateTime.now().add(const Duration(hours: 1)), DocumentReference? eventTypeRef;
basePrice: (map['BasePrice'] ?? map['Price'] ?? 0.0).toDouble(),
installationTime: map['InstallationTime'] ?? 0, if (map['EventType'] is DocumentReference) {
disassemblyTime: map['DisassemblyTime'] ?? 0, eventTypeRef = map['EventType'] as DocumentReference;
eventTypeId: map['EventType'] is DocumentReference eventTypeId = eventTypeRef.id;
? (map['EventType'] as DocumentReference).id } else if (map['EventType'] is String) {
: map['EventType'] ?? '', eventTypeId = map['EventType'] as String;
eventTypeRef: map['EventType'] is DocumentReference }
? map['EventType'] as DocumentReference
: null, // Gestion sécurisée du customer
customerId: map['customer'] is DocumentReference String customerId = '';
? (map['customer'] as DocumentReference).id if (map['customer'] is DocumentReference) {
: '', customerId = (map['customer'] as DocumentReference).id;
address: map['Address'] ?? '', } else if (map['customer'] is String) {
latitude: (map['Latitude'] ?? 0.0).toDouble(), customerId = map['customer'] as String;
longitude: (map['Longitude'] ?? 0.0).toDouble(), }
workforce: workforceRefs.whereType<DocumentReference>().toList(),
documents: docs, return EventModel(
options: options, id: id,
status: eventStatusFromString(map['status'] as String?), name: (map['Name'] ?? '').toString().trim(),
); description: (map['Description'] ?? '').toString(),
startDateTime: startDate,
endDateTime: endDate,
basePrice: _parseDouble(map['BasePrice'] ?? map['Price'] ?? 0.0),
installationTime: _parseInt(map['InstallationTime'] ?? 0),
disassemblyTime: _parseInt(map['DisassemblyTime'] ?? 0),
eventTypeId: eventTypeId,
eventTypeRef: eventTypeRef,
customerId: customerId,
address: (map['Address'] ?? '').toString(),
latitude: _parseDouble(map['Latitude'] ?? 0.0),
longitude: _parseDouble(map['Longitude'] ?? 0.0),
workforce: safeWorkforce,
documents: docs,
options: options,
status: eventStatusFromString(map['status'] as String?),
);
} catch (e) {
print('Error parsing event $id: $e');
print('Event data: $map');
rethrow;
}
}
// Méthodes utilitaires pour le parsing sécurisé
static double _parseDouble(dynamic value) {
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) {
final parsed = double.tryParse(value);
if (parsed != null) return parsed;
}
return 0.0;
}
static int _parseInt(dynamic value) {
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) {
final parsed = int.tryParse(value);
if (parsed != null) return parsed;
}
return 0;
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@@ -139,8 +206,12 @@ class EventModel {
'BasePrice': basePrice, 'BasePrice': basePrice,
'InstallationTime': installationTime, 'InstallationTime': installationTime,
'DisassemblyTime': disassemblyTime, 'DisassemblyTime': disassemblyTime,
'EventType': eventTypeId.isNotEmpty ? FirebaseFirestore.instance.collection('eventTypes').doc(eventTypeId) : null, 'EventType': eventTypeId.isNotEmpty
'customer': customerId.isNotEmpty ? FirebaseFirestore.instance.collection('customers').doc(customerId) : null, ? FirebaseFirestore.instance.collection('eventTypes').doc(eventTypeId)
: null,
'customer': customerId.isNotEmpty
? FirebaseFirestore.instance.collection('customers').doc(customerId)
: null,
'Address': address, 'Address': address,
'Position': GeoPoint(latitude, longitude), 'Position': GeoPoint(latitude, longitude),
'Latitude': latitude, 'Latitude': latitude,

View File

@@ -6,7 +6,7 @@ class EventOption {
final String details; final String details;
final double valMin; final double valMin;
final double valMax; final double valMax;
final List<DocumentReference> eventTypes; final List<String> eventTypes; // Changé de List<DocumentReference> à List<String>
EventOption({ EventOption({
required this.id, required this.id,
@@ -25,7 +25,7 @@ class EventOption {
valMin: (map['valMin'] ?? 0.0).toDouble(), valMin: (map['valMin'] ?? 0.0).toDouble(),
valMax: (map['valMax'] ?? 0.0).toDouble(), valMax: (map['valMax'] ?? 0.0).toDouble(),
eventTypes: (map['eventTypes'] as List<dynamic>? ?? []) eventTypes: (map['eventTypes'] as List<dynamic>? ?? [])
.whereType<DocumentReference>() .map((e) => e.toString()) // Convertit en String (supporte IDs et références)
.toList(), .toList(),
); );
} }

View File

@@ -19,33 +19,61 @@ class EventProvider with ChangeNotifier {
try { try {
print( print(
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)'); 'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
QuerySnapshot eventsSnapshot;
// On charge tous les events pour les users non-admins aussi
eventsSnapshot = await _firestore.collection('events').get();
print('Found ${eventsSnapshot.docs.length} events for user'); QuerySnapshot eventsSnapshot = await _firestore.collection('events').get();
print('Found ${eventsSnapshot.docs.length} events total');
// On filtre côté client si l'utilisateur n'est pas admin List<EventModel> allEvents = [];
final allEvents = eventsSnapshot.docs.map((doc) { int failedCount = 0;
print('Event data: ${doc.data()}');
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id); // Parser chaque événement individuellement pour éviter qu'une erreur interrompe tout
}).toList(); for (var doc in eventsSnapshot.docs) {
if (canViewAllEvents) { try {
_events = allEvents; final data = doc.data() as Map<String, dynamic>;
} else { print('Processing event ${doc.id}: ${data['Name'] ?? 'Unknown'}');
final userRef = _firestore.collection('users').doc(userId);
_events = allEvents final event = EventModel.fromMap(data, doc.id);
.where((e) => e.workforce.any((ref) => ref.id == userRef.id)) allEvents.add(event);
.toList(); } catch (e) {
print('Failed to parse event ${doc.id}: $e');
failedCount++;
// Continue avec les autres événements au lieu d'arrêter
}
} }
print('Parsed ${_events.length} events'); print('Successfully parsed ${allEvents.length} events, failed: $failedCount');
// Filtrage amélioré pour les utilisateurs non-admin
if (canViewAllEvents) {
_events = allEvents;
print('Admin user: showing all ${_events.length} events');
} else {
// Créer la référence utilisateur correctement
final userDocRef = _firestore.collection('users').doc(userId);
_events = allEvents.where((event) {
// Vérifier si l'utilisateur est dans l'équipe de l'événement
bool isInWorkforce = event.workforce.any((workforceRef) {
return workforceRef.path == userDocRef.path;
});
if (isInWorkforce) {
print('Event ${event.name} includes user ${userId}');
}
return isInWorkforce;
}).toList();
print('Non-admin user: showing ${_events.length} events out of ${allEvents.length}');
}
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
print('Error loading events: $e'); print('Error loading events: $e');
_isLoading = false; _isLoading = false;
_events = []; // S'assurer que la liste est vide en cas d'erreur
notifyListeners(); notifyListeners();
rethrow; rethrow;
} }

View File

@@ -78,6 +78,64 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
} }
} }
Future<void> _deleteEvent() async {
if (widget.event == null) return;
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer l\'événement'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Êtes-vous sûr de vouloir supprimer cet événement ?'),
const SizedBox(height: 8),
Text(
'Nom : ${widget.event!.name}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Cette action est irréversible.',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Supprimer'),
),
],
),
);
if (shouldDelete == true) {
final success = await _controller.deleteEvent(context, widget.event!.id);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Événement supprimé avec succès'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600; final isMobile = MediaQuery.of(context).size.width < 600;
@@ -153,12 +211,12 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
OptionSelectorWidget( OptionSelectorWidget(
eventType: selectedEventTypeName, eventType: controller.selectedEventTypeId, // Utilise l'ID au lieu du nom
selectedOptions: controller.selectedOptions, selectedOptions: controller.selectedOptions,
onChanged: controller.setSelectedOptions, onChanged: controller.setSelectedOptions,
onRemove: (name) { onRemove: (optionId) {
final newOptions = List<Map<String, dynamic>>.from(controller.selectedOptions); final newOptions = List<Map<String, dynamic>>.from(controller.selectedOptions);
newOptions.removeWhere((o) => o['name'] == name); newOptions.removeWhere((o) => o['id'] == optionId);
controller.setSelectedOptions(newOptions); controller.setSelectedOptions(newOptions);
}, },
eventTypeRequired: controller.selectedEventTypeId == null, eventTypeRequired: controller.selectedEventTypeId == null,
@@ -215,6 +273,7 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
onSubmit: _submit, onSubmit: _submit,
onSetConfirmed: !isEditMode ? () { onSetConfirmed: !isEditMode ? () {
} : null, } : null,
onDelete: isEditMode ? _deleteEvent : null, // Ajout du callback de suppression
), ),
], ],
), ),

View File

@@ -13,6 +13,7 @@ import 'package:em2rp/views/widgets/user_management/user_card.dart';
import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
import 'package:em2rp/views/event_add_page.dart'; import 'package:em2rp/views/event_add_page.dart';
import 'package:em2rp/views/widgets/event_form/event_options_display_widget.dart';
class EventDetails extends StatelessWidget { class EventDetails extends StatelessWidget {
final EventModel event; final EventModel event;
@@ -87,23 +88,29 @@ class EventDetails extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, // Optionnel mais recommandé pour bien aligner //Titre de l'événement
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// On remplace le SelectableText par une Column Expanded(
Expanded( // Utiliser Expanded pour que le texte ne déborde pas
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // Aligne les textes à gauche crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 1. Votre titre original Row(
SelectableText( crossAxisAlignment: CrossAxisAlignment.start,
event.name, children: [
style: Theme.of(context).textTheme.headlineMedium?.copyWith( SelectableText(
color: AppColors.noir, event.name,
fontWeight: FontWeight.bold, style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 12),
_buildStatusIcon(event.status),
],
), ),
),
const SizedBox(height: 4), const SizedBox(height: 4),
//Type d'événement
Text( Text(
event.eventTypeId, event.eventTypeId,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
@@ -113,8 +120,8 @@ class EventDetails extends StatelessWidget {
], ],
), ),
), ),
const SizedBox(width: 12), // Statut de l'événement
_buildStatusIcon(event.status),
const SizedBox(width: 8), const SizedBox(width: 8),
Spacer(), Spacer(),
if (Provider.of<LocalUserProvider>(context, listen: false) if (Provider.of<LocalUserProvider>(context, listen: false)
@@ -168,13 +175,13 @@ class EventDetails extends StatelessWidget {
_buildInfoRow( _buildInfoRow(
context, context,
Icons.calendar_today, Icons.calendar_today,
'Date de début', 'Horaire de début',
dateFormat.format(event.startDateTime), dateFormat.format(event.startDateTime),
), ),
_buildInfoRow( _buildInfoRow(
context, context,
Icons.calendar_today, Icons.calendar_today,
'Date de fin', 'Horaire de fin',
dateFormat.format(event.endDateTime), dateFormat.format(event.endDateTime),
), ),
if (canViewPrices) if (canViewPrices)
@@ -185,82 +192,50 @@ class EventDetails extends StatelessWidget {
currencyFormat.format(event.basePrice), currencyFormat.format(event.basePrice),
), ),
if (event.options.isNotEmpty) ...[ if (event.options.isNotEmpty) ...[
const SizedBox(height: 8), EventOptionsDisplayWidget(
Text('Options sélectionnées', optionsData: event.options,
style: canViewPrices: canViewPrices,
Theme.of(context).textTheme.titleLarge?.copyWith( showPriceCalculation: false, // On affiche le total séparément
color: AppColors.noir,
fontWeight: FontWeight.bold,
)),
const SizedBox(height: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: event.options.map((opt) {
final price = (opt['price'] ?? 0.0) as num;
final isNegative = price < 0;
return ListTile(
leading: Icon(Icons.tune,
color:
isNegative ? Colors.red : AppColors.rouge),
title: Text(opt['name'] ?? '',
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(opt['details'] ?? ''),
trailing: canViewPrices
? Text(
(isNegative ? '- ' : '+ ') +
currencyFormat.format(price.abs()),
style: TextStyle(
color: isNegative
? Colors.red
: AppColors.noir,
fontWeight: FontWeight.bold,
),
)
: null,
contentPadding: EdgeInsets.zero,
dense: true,
);
}).toList(),
), ),
if (canViewPrices) ...[ ],
const SizedBox(height: 4), if (canViewPrices) ...[
Builder( const SizedBox(height: 4),
builder: (context) { Builder(
final total = event.basePrice + builder: (context) {
event.options.fold<num>(0, final total = event.basePrice +
(sum, opt) => sum + (opt['price'] ?? 0.0)); event.options.fold<num>(0,
return Padding( (sum, opt) => sum + (opt['price'] ?? 0.0));
padding: return Padding(
const EdgeInsets.only(top: 8.0, bottom: 8.0), padding:
child: Row( const EdgeInsets.only(top: 8.0, bottom: 8.0),
children: [ child: Row(
const Icon(Icons.attach_money, children: [
color: AppColors.rouge), const Icon(Icons.attach_money,
const SizedBox(width: 8), color: AppColors.rouge),
Text('Prix total : ', const SizedBox(width: 8),
style: Theme.of(context) Text('Prix total : ',
.textTheme
.titleMedium
?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
)),
Text(
currencyFormat.format(total),
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium .titleMedium
?.copyWith( ?.copyWith(
color: AppColors.rouge, color: AppColors.noir,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), )),
), Text(
], currencyFormat.format(total),
), style: Theme.of(context)
); .textTheme
}, .titleMedium
), ?.copyWith(
], color: AppColors.rouge,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
], ],
_buildInfoRow( _buildInfoRow(
context, context,
@@ -268,12 +243,39 @@ class EventDetails extends StatelessWidget {
'Temps d\'installation', 'Temps d\'installation',
'${event.installationTime} heures', '${event.installationTime} heures',
), ),
// Sous-titre: Horaire d'arrivée prévisionnelle (début - installation)
Builder(
builder: (context) {
final arrival = event.startDateTime.subtract(Duration(hours: event.installationTime));
return Padding(
padding: const EdgeInsets.only(left: 36.0, bottom: 4.0),
child: Text(
'Horaire d\'arrivée prévisionnel : ${DateFormat('dd/MM/yyyy HH:mm').format(arrival)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[700]),
),
);
},
),
_buildInfoRow( _buildInfoRow(
context, context,
Icons.construction, Icons.construction,
'Temps de démontage', 'Temps de démontage',
'${event.disassemblyTime} heures', '${event.disassemblyTime} heures',
), ),
// Sous-titre: Horaire de départ prévu (fin + démontage)
Builder(
builder: (context) {
final departure = event.endDateTime.add(Duration(hours: event.disassemblyTime));
return Padding(
padding: const EdgeInsets.only(left: 36.0, bottom: 4.0),
child: Text(
'Horaire de départ prévu : ${DateFormat('dd/MM/yyyy HH:mm').format(departure)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[700]),
),
);
},
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Description', 'Description',

View File

@@ -40,7 +40,7 @@ class EventBasicInfoSection extends StatelessWidget {
TextFormField( TextFormField(
controller: nameController, controller: nameController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Nom de l\'événement', labelText: 'Nom de l\'événement*',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.event), prefixIcon: Icon(Icons.event),
), ),
@@ -50,29 +50,40 @@ class EventBasicInfoSection extends StatelessWidget {
if (isLoadingEventTypes) if (isLoadingEventTypes)
const Center(child: CircularProgressIndicator()) const Center(child: CircularProgressIndicator())
else else
DropdownButtonFormField<String>( Row(
value: selectedEventTypeId, mainAxisSize: MainAxisSize.min,
items: eventTypes children: [
.map((type) => DropdownMenuItem<String>( Flexible(
value: type.id, fit: FlexFit.loose,
child: Text(type.name), child: DropdownButtonFormField<String>(
)) initialValue: selectedEventTypeId,
.toList(), items: eventTypes
onChanged: onEventTypeChanged, .map((type) => DropdownMenuItem<String>(
decoration: const InputDecoration( value: type.id,
labelText: 'Type d\'événement', child: Text(type.name),
border: OutlineInputBorder(), ))
prefixIcon: Icon(Icons.category), .toList(),
), onChanged: onEventTypeChanged,
validator: (v) => v == null ? 'Sélectionnez un type' : null, 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),
),
],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildDateTimeRow(context),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: basePriceController, controller: basePriceController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Prix de base (€)', labelText: 'Prix de base (€)*',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.euro), prefixIcon: Icon(Icons.euro),
hintText: '1050.50', hintText: '1050.50',
@@ -120,7 +131,7 @@ class EventBasicInfoSection extends StatelessWidget {
child: TextFormField( child: TextFormField(
readOnly: true, readOnly: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Début', labelText: 'Début*',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today), prefixIcon: Icon(Icons.calendar_today),
suffixIcon: Icon(Icons.edit_calendar), suffixIcon: Icon(Icons.edit_calendar),
@@ -143,7 +154,7 @@ class EventBasicInfoSection extends StatelessWidget {
child: TextFormField( child: TextFormField(
readOnly: true, readOnly: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Fin', labelText: 'Fin*',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today), prefixIcon: Icon(Icons.calendar_today),
suffixIcon: Icon(Icons.edit_calendar), suffixIcon: Icon(Icons.edit_calendar),

View File

@@ -93,11 +93,11 @@ class _EventDetailsSectionState extends State<EventDetailsSection> {
), ),
], ],
), ),
_buildSectionTitle('Adresse'), _buildSectionTitle('Adresse*'),
TextFormField( TextFormField(
controller: widget.addressController, controller: widget.addressController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Adresse', labelText: 'Adresse*',
border: OutlineInputBorder(), border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on), prefixIcon: Icon(Icons.location_on),
), ),

View File

@@ -6,6 +6,7 @@ class EventFormActions extends StatelessWidget {
final VoidCallback onCancel; final VoidCallback onCancel;
final VoidCallback onSubmit; final VoidCallback onSubmit;
final VoidCallback? onSetConfirmed; final VoidCallback? onSetConfirmed;
final VoidCallback? onDelete; // Nouveau paramètre pour la suppression
const EventFormActions({ const EventFormActions({
super.key, super.key,
@@ -14,6 +15,7 @@ class EventFormActions extends StatelessWidget {
required this.onCancel, required this.onCancel,
required this.onSubmit, required this.onSubmit,
this.onSetConfirmed, this.onSetConfirmed,
this.onDelete, // Paramètre optionnel pour la suppression
}); });
@override @override
@@ -22,23 +24,43 @@ class EventFormActions extends StatelessWidget {
children: [ children: [
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TextButton( // Bouton de suppression en mode édition
onPressed: isLoading ? null : onCancel, if (isEditMode && onDelete != null)
child: const Text('Annuler'), ElevatedButton.icon(
), icon: const Icon(Icons.delete, color: Colors.white),
const SizedBox(width: 8), label: const Text('Supprimer'),
ElevatedButton.icon( style: ElevatedButton.styleFrom(
icon: const Icon(Icons.check), backgroundColor: Colors.red,
onPressed: isLoading ? null : onSubmit, foregroundColor: Colors.white,
label: isLoading ),
? const SizedBox( onPressed: isLoading ? null : onDelete,
width: 20, )
height: 20, else
child: CircularProgressIndicator(strokeWidth: 2), const SizedBox.shrink(), // Espace vide si pas en mode édition
)
: Text(isEditMode ? 'Enregistrer' : 'Créer'), // 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'),
),
],
), ),
], ],
), ),

View File

@@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/option_model.dart';
import 'package:intl/intl.dart';
import 'package:em2rp/utils/colors.dart';
class EventOptionsDisplayWidget extends StatelessWidget {
final List<Map<String, dynamic>> optionsData;
final bool canViewPrices;
final bool showPriceCalculation;
const EventOptionsDisplayWidget({
super.key,
required this.optionsData,
this.canViewPrices = true,
this.showPriceCalculation = true,
});
@override
Widget build(BuildContext context) {
if (optionsData.isEmpty) {
return const SizedBox.shrink();
}
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
'Options sélectionnées',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
FutureBuilder<List<Map<String, dynamic>>>(
future: _loadOptionsWithDetails(optionsData),
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),
),
);
}
final enrichedOptions = snapshot.data ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...enrichedOptions.map((opt) {
final price = (opt['price'] ?? 0.0) as num;
final isNegative = price < 0;
return ListTile(
leading: Icon(Icons.tune, color: AppColors.rouge),
title: Text(
opt['name'] ?? '',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: opt['details'] != null && opt['details'].toString().trim().isNotEmpty
? Text(
opt['details'].toString().trim(),
style: const TextStyle(
fontWeight: FontWeight.normal,
color: Colors.black87,
fontSize: 13,
fontStyle: FontStyle.italic,
),
)
: Text(
'Aucun détail disponible',
style: TextStyle(
fontWeight: FontWeight.normal,
color: Colors.grey[600],
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
trailing: canViewPrices
? Text(
(isNegative ? '- ' : '+ ') +
currencyFormat.format(price.abs()),
style: TextStyle(
color: isNegative ? Colors.red : AppColors.noir,
fontWeight: FontWeight.bold,
),
)
: null,
contentPadding: EdgeInsets.zero,
dense: true,
);
}),
if (canViewPrices && showPriceCalculation) ...[
const SizedBox(height: 4),
_buildTotalPrice(context, enrichedOptions, 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));
return Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Row(
children: [
const Icon(Icons.tune, color: AppColors.rouge),
const SizedBox(width: 8),
Text(
'Total options : ',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
Text(
currencyFormat.format(optionsTotal),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: AppColors.rouge,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
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 complets depuis Firestore
if (optionData['id'] != null) {
final doc = await FirebaseFirestore.instance
.collection('options')
.doc(optionData['id'])
.get();
if (doc.exists) {
final firestoreData = doc.data()!;
// Combiner les données Firestore avec le prix choisi
enrichedOptions.add({
'id': optionData['id'],
'name': firestoreData['name'], // Récupéré depuis Firestore
'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore
'price': optionData['price'], // Prix choisi par l'utilisateur
'valMin': firestoreData['valMin'],
'valMax': firestoreData['valMax'],
});
} else {
// Option supprimée de Firestore, afficher avec des données par défaut
enrichedOptions.add({
'id': optionData['id'],
'name': 'Option supprimée (ID: ${optionData['id']})',
'details': 'Cette option n\'existe plus dans la base de données',
'price': optionData['price'],
});
}
} else {
// Ancien format sans ID (rétrocompatibilité)
// Utiliser les données locales disponibles
enrichedOptions.add({
'name': optionData['name'] ?? 'Option inconnue',
'details': optionData['details'] ?? 'Aucun détail disponible',
'price': optionData['price'] ?? 0.0,
});
}
} catch (e) {
print('Erreur lors du chargement de l\'option ${optionData['id']}: $e');
// En cas d'erreur, créer une entrée avec les données disponibles
enrichedOptions.add({
'id': optionData['id'],
'name': 'Erreur de chargement (ID: ${optionData['id']})',
'details': 'Impossible de charger les détails de cette option',
'price': optionData['price'] ?? 0.0,
});
}
}
return enrichedOptions;
}
}

View File

@@ -3,16 +3,16 @@ import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/option_model.dart'; import 'package:em2rp/models/option_model.dart';
class OptionSelectorWidget extends StatefulWidget { class OptionSelectorWidget extends StatefulWidget {
final String? eventType;
final List<Map<String, dynamic>> selectedOptions; final List<Map<String, dynamic>> selectedOptions;
final ValueChanged<List<Map<String, dynamic>>> onChanged; 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 eventTypeRequired;
final bool isMobile; final bool isMobile;
final String? eventType;
const OptionSelectorWidget({ const OptionSelectorWidget({
super.key, Key? key,
required this.eventType, this.eventType,
required this.selectedOptions, required this.selectedOptions,
required this.onChanged, required this.onChanged,
this.onRemove, this.onRemove,
@@ -27,15 +27,11 @@ class OptionSelectorWidget extends StatefulWidget {
class _OptionSelectorWidgetState extends State<OptionSelectorWidget> { class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
List<EventOption> _allOptions = []; List<EventOption> _allOptions = [];
bool _loading = true; bool _loading = true;
final String _search = ''; int _rebuildKey = 0; // Clé pour forcer la reconstruction du FutureBuilder
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
@override @override
void didUpdateWidget(covariant OptionSelectorWidget oldWidget) { void didUpdateWidget(covariant OptionSelectorWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.eventType != widget.eventType) {
_fetchOptions();
}
} }
@override @override
@@ -62,7 +58,7 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
context: context, context: context,
builder: (ctx) => _OptionPickerDialog( builder: (ctx) => _OptionPickerDialog(
allOptions: _allOptions, allOptions: _allOptions,
eventType: widget.eventType, eventType: widget.eventType, // Ajout du paramètre manquant
), ),
); );
if (selected != null) { if (selected != null) {
@@ -81,61 +77,104 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
Text('Options sélectionnées', Text('Options sélectionnées',
style: Theme.of(context).textTheme.titleMedium), style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
Column(
children: widget.selectedOptions // Affichage direct des options sélectionnées avec clé de reconstruction
.map((opt) => Card( widget.selectedOptions.isEmpty
elevation: widget.isMobile ? 0 : 2, ? const Padding(
margin: EdgeInsets.symmetric( padding: EdgeInsets.all(16.0),
vertical: widget.isMobile ? 4 : 8, child: Text('Aucune option sélectionnée'),
horizontal: widget.isMobile ? 0 : 8), )
shape: RoundedRectangleBorder( : FutureBuilder<List<Map<String, dynamic>>>(
borderRadius: key: ValueKey(_rebuildKey), // Clé pour forcer la reconstruction
BorderRadius.circular(widget.isMobile ? 8 : 12)), future: _loadOptionsWithDetails(widget.selectedOptions),
child: Padding( builder: (context, snapshot) {
padding: EdgeInsets.all(widget.isMobile ? 8.0 : 12.0), if (snapshot.connectionState == ConnectionState.waiting) {
child: Row( return const Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: EdgeInsets.all(16.0),
children: [ child: Center(child: CircularProgressIndicator()),
Expanded( );
child: Column( }
crossAxisAlignment: CrossAxisAlignment.start,
children: [ if (snapshot.hasError) {
Text(opt['name'] ?? '', return Padding(
style: const TextStyle( padding: const EdgeInsets.all(16.0),
fontWeight: FontWeight.bold)), child: Text(
if (opt['details'] != null && 'Erreur lors du chargement des options: ${snapshot.error}',
opt['details'] != '') style: const TextStyle(color: Colors.red),
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);
}
},
),
],
), ),
), );
)) }
.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), const SizedBox(height: 16),
Center( Center(
child: ElevatedButton.icon( 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 { class _OptionPickerDialog extends StatefulWidget {
final List<EventOption> allOptions; final List<EventOption> allOptions;
final String? eventType; final String? eventType;
const _OptionPickerDialog(
{required this.allOptions, required this.eventType}); const _OptionPickerDialog({
required this.allOptions,
this.eventType,
});
@override @override
State<_OptionPickerDialog> createState() => _OptionPickerDialogState(); State<_OptionPickerDialog> createState() => _OptionPickerDialogState();
} }
class _OptionPickerDialogState extends State<_OptionPickerDialog> { class _OptionPickerDialogState extends State<_OptionPickerDialog> {
String _search = ''; String _search = '';
bool _creating = false; bool _creating = false;
@override @override
Widget build(BuildContext context) { 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) { final filtered = widget.allOptions.where((opt) {
if (widget.eventType == null) return false; print('Option: ${opt.name}');
final matchesType = print(' opt.eventTypes: ${opt.eventTypes}');
opt.eventTypes.any((ref) => ref.id == widget.eventType); print(' widget.eventType: ${widget.eventType}');
final matchesSearch =
opt.name.toLowerCase().contains(_search.toLowerCase()); if (widget.eventType == null) {
return matchesType && matchesSearch; 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(); }).toList();
print('Filtered options count: ${filtered.length}');
print('===========================');
return Dialog( return Dialog(
child: SizedBox( child: SizedBox(
width: 400, width: 400,
@@ -243,11 +355,8 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
); );
if (price != null) { if (price != null) {
Navigator.pop(context, { Navigator.pop(context, {
'name': opt.name, 'id': opt.id, // ID de l'option (obligatoire pour récupérer les données)
'price': price, 'price': price, // Prix choisi par l'utilisateur (obligatoire car personnalisé)
'compatibleTypes': opt.eventTypes
.map((ref) => ref.id)
.toList(),
}); });
} }
}, },
@@ -304,9 +413,10 @@ 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 = [];
final List<String> _allTypes = ['Bal', 'Mariage', 'Anniversaire'];
String? _error; String? _error;
bool _checkingName = false; bool _checkingName = false;
List<Map<String,dynamic>> _allEventTypes = [];
bool _loading = true;
Future<bool> _isNameUnique(String name) async { Future<bool> _isNameUnique(String name) async {
final snap = await FirebaseFirestore.instance final snap = await FirebaseFirestore.instance
@@ -316,6 +426,23 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
return snap.docs.isEmpty; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
@@ -374,21 +501,21 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
const Text('Types d\'événement associés :'), const Text('Types d\'événement associés :'),
Wrap( Wrap(
spacing: 8, spacing: 8,
children: _allTypes children: _allEventTypes
.map((type) => FilterChip( .map((type) => FilterChip(
label: Text(type), label: Text(type['name']),
selected: _selectedTypes.contains(type), selected: _selectedTypes.contains(type['id']),
onSelected: (selected) { onSelected: (selected) {
setState(() { setState(() {
if (selected) { if (selected) {
_selectedTypes.add(type); _selectedTypes.add(type['id']);
} else { } else {
_selectedTypes.remove(type); _selectedTypes.remove(type['id']);
} }
}); });
}, },
)) ))
.toList(), .toList(),
), ),
], ],
), ),
@@ -435,22 +562,19 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
() => _error = 'Ce nom d\'option est déjà utilisé.'); () => _error = 'Ce nom d\'option est déjà utilisé.');
return; return;
} }
final eventTypeRefs = _selectedTypes
.map((type) => FirebaseFirestore.instance
.collection('eventTypes')
.doc(type))
.toList();
try { try {
// Debug : afficher le contenu envoyé
print('Enregistrement option avec eventTypes : ' + _selectedTypes.toString() + '\u001b[0m');
await FirebaseFirestore.instance.collection('options').add({ await FirebaseFirestore.instance.collection('options').add({
'name': name, 'name': name,
'details': _detailsController.text.trim(), 'details': _detailsController.text.trim(),
'valMin': min, 'valMin': min,
'valMax': max, 'valMax': max,
'eventTypes': eventTypeRefs, 'eventTypes': _selectedTypes,
}); });
Navigator.pop(context, true); Navigator.pop(context, true);
} catch (e) { } 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 child: _checkingName