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:
@@ -1,10 +1,10 @@
|
||||
class Env {
|
||||
static const bool isDevelopment = false;
|
||||
static const bool isDevelopment = true;
|
||||
|
||||
// Configuration de l'auto-login en développement
|
||||
static const String devAdminEmail = 'paul.fournel@em2events.fr';
|
||||
static const String devAdminPassword =
|
||||
"Azerty\$1!"; // À remplacer par le vrai mot de passe
|
||||
"Pastis51!"; // À remplacer par le vrai mot de passe
|
||||
|
||||
// URLs et endpoints
|
||||
static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com';
|
||||
|
||||
@@ -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() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
@@ -54,7 +54,6 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print("test");
|
||||
return MaterialApp(
|
||||
title: 'EM2 ERP',
|
||||
theme: ThemeData(
|
||||
|
||||
@@ -72,62 +72,129 @@ class EventModel {
|
||||
});
|
||||
|
||||
factory EventModel.fromMap(Map<String, dynamic> map, String id) {
|
||||
try {
|
||||
// Gestion sécurisée des références workforce
|
||||
final List<dynamic> workforceRefs = map['workforce'] ?? [];
|
||||
final List<DocumentReference> safeWorkforce = [];
|
||||
|
||||
for (var ref in workforceRefs) {
|
||||
if (ref is DocumentReference) {
|
||||
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 docs = docsRaw is List
|
||||
? docsRaw.map<Map<String, String>>((e) {
|
||||
final List<Map<String, String>> docs = [];
|
||||
|
||||
if (docsRaw is List) {
|
||||
for (var e in docsRaw) {
|
||||
try {
|
||||
if (e is Map) {
|
||||
return Map<String, String>.from(e);
|
||||
docs.add(Map<String, String>.from(e));
|
||||
} else if (e is String) {
|
||||
final fileName = Uri.decodeComponent(
|
||||
e.split('/').last.split('?').first,
|
||||
);
|
||||
return {'name': fileName, 'url': e};
|
||||
} else {
|
||||
return {};
|
||||
docs.add({'name': fileName, 'url': e});
|
||||
}
|
||||
}).toList()
|
||||
: <Map<String, String>>[];
|
||||
} catch (docError) {
|
||||
print('Warning: Failed to parse document in event $id: $docError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion sécurisée des options
|
||||
final optionsRaw = map['options'] ?? [];
|
||||
final options = optionsRaw is List
|
||||
? optionsRaw.map<Map<String, dynamic>>((e) {
|
||||
final List<Map<String, dynamic>> options = [];
|
||||
|
||||
if (optionsRaw is List) {
|
||||
for (var e in optionsRaw) {
|
||||
try {
|
||||
if (e is Map) {
|
||||
return Map<String, dynamic>.from(e);
|
||||
} else {
|
||||
return {};
|
||||
options.add(Map<String, dynamic>.from(e));
|
||||
}
|
||||
}).toList()
|
||||
: <Map<String, dynamic>>[];
|
||||
} catch (optionError) {
|
||||
print('Warning: Failed to parse option in event $id: $optionError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion sécurisée de l'EventType
|
||||
String eventTypeId = '';
|
||||
DocumentReference? eventTypeRef;
|
||||
|
||||
if (map['EventType'] is DocumentReference) {
|
||||
eventTypeRef = map['EventType'] as DocumentReference;
|
||||
eventTypeId = eventTypeRef.id;
|
||||
} else if (map['EventType'] is String) {
|
||||
eventTypeId = map['EventType'] as String;
|
||||
}
|
||||
|
||||
// Gestion sécurisée du customer
|
||||
String customerId = '';
|
||||
if (map['customer'] is DocumentReference) {
|
||||
customerId = (map['customer'] as DocumentReference).id;
|
||||
} else if (map['customer'] is String) {
|
||||
customerId = map['customer'] as String;
|
||||
}
|
||||
|
||||
return EventModel(
|
||||
id: id,
|
||||
name: map['Name'] ?? '',
|
||||
description: map['Description'] ?? '',
|
||||
startDateTime: startTimestamp?.toDate() ?? DateTime.now(),
|
||||
endDateTime: endTimestamp?.toDate() ??
|
||||
DateTime.now().add(const Duration(hours: 1)),
|
||||
basePrice: (map['BasePrice'] ?? map['Price'] ?? 0.0).toDouble(),
|
||||
installationTime: map['InstallationTime'] ?? 0,
|
||||
disassemblyTime: map['DisassemblyTime'] ?? 0,
|
||||
eventTypeId: map['EventType'] is DocumentReference
|
||||
? (map['EventType'] as DocumentReference).id
|
||||
: map['EventType'] ?? '',
|
||||
eventTypeRef: map['EventType'] is DocumentReference
|
||||
? map['EventType'] as DocumentReference
|
||||
: null,
|
||||
customerId: map['customer'] is DocumentReference
|
||||
? (map['customer'] as DocumentReference).id
|
||||
: '',
|
||||
address: map['Address'] ?? '',
|
||||
latitude: (map['Latitude'] ?? 0.0).toDouble(),
|
||||
longitude: (map['Longitude'] ?? 0.0).toDouble(),
|
||||
workforce: workforceRefs.whereType<DocumentReference>().toList(),
|
||||
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() {
|
||||
@@ -139,8 +206,12 @@ class EventModel {
|
||||
'BasePrice': basePrice,
|
||||
'InstallationTime': installationTime,
|
||||
'DisassemblyTime': disassemblyTime,
|
||||
'EventType': eventTypeId.isNotEmpty ? FirebaseFirestore.instance.collection('eventTypes').doc(eventTypeId) : null,
|
||||
'customer': customerId.isNotEmpty ? FirebaseFirestore.instance.collection('customers').doc(customerId) : null,
|
||||
'EventType': eventTypeId.isNotEmpty
|
||||
? FirebaseFirestore.instance.collection('eventTypes').doc(eventTypeId)
|
||||
: null,
|
||||
'customer': customerId.isNotEmpty
|
||||
? FirebaseFirestore.instance.collection('customers').doc(customerId)
|
||||
: null,
|
||||
'Address': address,
|
||||
'Position': GeoPoint(latitude, longitude),
|
||||
'Latitude': latitude,
|
||||
|
||||
@@ -6,7 +6,7 @@ class EventOption {
|
||||
final String details;
|
||||
final double valMin;
|
||||
final double valMax;
|
||||
final List<DocumentReference> eventTypes;
|
||||
final List<String> eventTypes; // Changé de List<DocumentReference> à List<String>
|
||||
|
||||
EventOption({
|
||||
required this.id,
|
||||
@@ -25,7 +25,7 @@ class EventOption {
|
||||
valMin: (map['valMin'] ?? 0.0).toDouble(),
|
||||
valMax: (map['valMax'] ?? 0.0).toDouble(),
|
||||
eventTypes: (map['eventTypes'] as List<dynamic>? ?? [])
|
||||
.whereType<DocumentReference>()
|
||||
.map((e) => e.toString()) // Convertit en String (supporte IDs et références)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,33 +19,61 @@ class EventProvider with ChangeNotifier {
|
||||
try {
|
||||
print(
|
||||
'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
|
||||
final allEvents = eventsSnapshot.docs.map((doc) {
|
||||
print('Event data: ${doc.data()}');
|
||||
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||
}).toList();
|
||||
if (canViewAllEvents) {
|
||||
_events = allEvents;
|
||||
} else {
|
||||
final userRef = _firestore.collection('users').doc(userId);
|
||||
_events = allEvents
|
||||
.where((e) => e.workforce.any((ref) => ref.id == userRef.id))
|
||||
.toList();
|
||||
List<EventModel> allEvents = [];
|
||||
int failedCount = 0;
|
||||
|
||||
// Parser chaque événement individuellement pour éviter qu'une erreur interrompe tout
|
||||
for (var doc in eventsSnapshot.docs) {
|
||||
try {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
print('Processing event ${doc.id}: ${data['Name'] ?? 'Unknown'}');
|
||||
|
||||
final event = EventModel.fromMap(data, doc.id);
|
||||
allEvents.add(event);
|
||||
} 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;
|
||||
notifyListeners();
|
||||
|
||||
} catch (e) {
|
||||
print('Error loading events: $e');
|
||||
_isLoading = false;
|
||||
_events = []; // S'assurer que la liste est vide en cas d'erreur
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
@@ -153,12 +211,12 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
OptionSelectorWidget(
|
||||
eventType: selectedEventTypeName,
|
||||
eventType: controller.selectedEventTypeId, // Utilise l'ID au lieu du nom
|
||||
selectedOptions: controller.selectedOptions,
|
||||
onChanged: controller.setSelectedOptions,
|
||||
onRemove: (name) {
|
||||
onRemove: (optionId) {
|
||||
final newOptions = List<Map<String, dynamic>>.from(controller.selectedOptions);
|
||||
newOptions.removeWhere((o) => o['name'] == name);
|
||||
newOptions.removeWhere((o) => o['id'] == optionId);
|
||||
controller.setSelectedOptions(newOptions);
|
||||
},
|
||||
eventTypeRequired: controller.selectedEventTypeId == null,
|
||||
@@ -215,6 +273,7 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
|
||||
onSubmit: _submit,
|
||||
onSetConfirmed: !isEditMode ? () {
|
||||
} : null,
|
||||
onDelete: isEditMode ? _deleteEvent : null, // Ajout du callback de suppression
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:em2rp/views/widgets/user_management/user_card.dart';
|
||||
import 'package:em2rp/models/user_model.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/widgets/event_form/event_options_display_widget.dart';
|
||||
|
||||
class EventDetails extends StatelessWidget {
|
||||
final EventModel event;
|
||||
@@ -87,14 +88,16 @@ class EventDetails extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, // Optionnel mais recommandé pour bien aligner
|
||||
//Titre de l'événement
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// On remplace le SelectableText par une Column
|
||||
Expanded( // Utiliser Expanded pour que le texte ne déborde pas
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, // Aligne les textes à gauche
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Votre titre original
|
||||
SelectableText(
|
||||
event.name,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
@@ -102,8 +105,12 @@ class EventDetails extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusIcon(event.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
//Type d'événement
|
||||
Text(
|
||||
event.eventTypeId,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
@@ -113,8 +120,8 @@ class EventDetails extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusIcon(event.status),
|
||||
// Statut de l'événement
|
||||
|
||||
const SizedBox(width: 8),
|
||||
Spacer(),
|
||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||
@@ -168,13 +175,13 @@ class EventDetails extends StatelessWidget {
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.calendar_today,
|
||||
'Date de début',
|
||||
'Horaire de début',
|
||||
dateFormat.format(event.startDateTime),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.calendar_today,
|
||||
'Date de fin',
|
||||
'Horaire de fin',
|
||||
dateFormat.format(event.endDateTime),
|
||||
),
|
||||
if (canViewPrices)
|
||||
@@ -185,43 +192,12 @@ class EventDetails extends StatelessWidget {
|
||||
currencyFormat.format(event.basePrice),
|
||||
),
|
||||
if (event.options.isNotEmpty) ...[
|
||||
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),
|
||||
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(),
|
||||
EventOptionsDisplayWidget(
|
||||
optionsData: event.options,
|
||||
canViewPrices: canViewPrices,
|
||||
showPriceCalculation: false, // On affiche le total séparément
|
||||
),
|
||||
],
|
||||
if (canViewPrices) ...[
|
||||
const SizedBox(height: 4),
|
||||
Builder(
|
||||
@@ -261,19 +237,45 @@ class EventDetails extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.build,
|
||||
'Temps d\'installation',
|
||||
'${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(
|
||||
context,
|
||||
Icons.construction,
|
||||
'Temps de démontage',
|
||||
'${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),
|
||||
Text(
|
||||
'Description',
|
||||
|
||||
@@ -40,7 +40,7 @@ class EventBasicInfoSection extends StatelessWidget {
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom de l\'événement',
|
||||
labelText: 'Nom de l\'événement*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.event),
|
||||
),
|
||||
@@ -50,8 +50,13 @@ class EventBasicInfoSection extends StatelessWidget {
|
||||
if (isLoadingEventTypes)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedEventTypeId,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: selectedEventTypeId,
|
||||
items: eventTypes
|
||||
.map((type) => DropdownMenuItem<String>(
|
||||
value: type.id,
|
||||
@@ -60,19 +65,25 @@ class EventBasicInfoSection extends StatelessWidget {
|
||||
.toList(),
|
||||
onChanged: onEventTypeChanged,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'événement',
|
||||
labelText: 'Type d\'événement*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
validator: (v) => v == null ? 'Sélectionnez un type' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDateTimeRow(context),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: _buildDateTimeRow(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: basePriceController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prix de base (€)',
|
||||
labelText: 'Prix de base (€)*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.euro),
|
||||
hintText: '1050.50',
|
||||
@@ -120,7 +131,7 @@ class EventBasicInfoSection extends StatelessWidget {
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Début',
|
||||
labelText: 'Début*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
suffixIcon: Icon(Icons.edit_calendar),
|
||||
@@ -143,7 +154,7 @@ class EventBasicInfoSection extends StatelessWidget {
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Fin',
|
||||
labelText: 'Fin*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
suffixIcon: Icon(Icons.edit_calendar),
|
||||
|
||||
@@ -93,11 +93,11 @@ class _EventDetailsSectionState extends State<EventDetailsSection> {
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildSectionTitle('Adresse'),
|
||||
_buildSectionTitle('Adresse*'),
|
||||
TextFormField(
|
||||
controller: widget.addressController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse',
|
||||
labelText: 'Adresse*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ class EventFormActions extends StatelessWidget {
|
||||
final VoidCallback onCancel;
|
||||
final VoidCallback onSubmit;
|
||||
final VoidCallback? onSetConfirmed;
|
||||
final VoidCallback? onDelete; // Nouveau paramètre pour la suppression
|
||||
|
||||
const EventFormActions({
|
||||
super.key,
|
||||
@@ -14,6 +15,7 @@ class EventFormActions extends StatelessWidget {
|
||||
required this.onCancel,
|
||||
required this.onSubmit,
|
||||
this.onSetConfirmed,
|
||||
this.onDelete, // Paramètre optionnel pour la suppression
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -22,7 +24,25 @@ class EventFormActions extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Bouton de suppression en mode édition
|
||||
if (isEditMode && onDelete != null)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.delete, color: Colors.white),
|
||||
label: const Text('Supprimer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: isLoading ? null : onDelete,
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(), // Espace vide si pas en mode édition
|
||||
|
||||
// Boutons Annuler et Enregistrer/Créer
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isLoading ? null : onCancel,
|
||||
@@ -42,6 +62,8 @@ class EventFormActions extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isEditMode && onSetConfirmed != null)
|
||||
Center(
|
||||
child: Padding(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,8 +77,38 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
Text('Options sélectionnées',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
children: widget.selectedOptions
|
||||
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final enrichedOptions = snapshot.data ?? [];
|
||||
|
||||
return Column(
|
||||
children: enrichedOptions
|
||||
.map((opt) => Card(
|
||||
elevation: widget.isMobile ? 0 : 2,
|
||||
margin: EdgeInsets.symmetric(
|
||||
@@ -104,7 +130,7 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
if (opt['details'] != null &&
|
||||
opt['details'] != '')
|
||||
opt['details'].toString().trim().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Text(opt['details'],
|
||||
@@ -119,14 +145,24 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: 'Supprimer cette option',
|
||||
onPressed: () {
|
||||
if (widget.onRemove != null) {
|
||||
widget.onRemove!(opt['name'] as String);
|
||||
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 {
|
||||
final newList = List<Map<String, dynamic>>.from(
|
||||
widget.selectedOptions)
|
||||
..removeWhere(
|
||||
(o) => o['name'] == opt['name']);
|
||||
// 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++;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -135,7 +171,10 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
),
|
||||
))
|
||||
.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,16 +501,16 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
const Text('Types d\'événement associés :'),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: _allTypes
|
||||
children: _allEventTypes
|
||||
.map((type) => FilterChip(
|
||||
label: Text(type),
|
||||
selected: _selectedTypes.contains(type),
|
||||
label: Text(type['name']),
|
||||
selected: _selectedTypes.contains(type['id']),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTypes.add(type);
|
||||
_selectedTypes.add(type['id']);
|
||||
} else {
|
||||
_selectedTypes.remove(type);
|
||||
_selectedTypes.remove(type['id']);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -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