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:
@@ -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,29 +50,40 @@ class EventBasicInfoSection extends StatelessWidget {
|
||||
if (isLoadingEventTypes)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedEventTypeId,
|
||||
items: eventTypes
|
||||
.map((type) => DropdownMenuItem<String>(
|
||||
value: type.id,
|
||||
child: Text(type.name),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: onEventTypeChanged,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'événement',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
validator: (v) => v == null ? 'Sélectionnez un type' : null,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: selectedEventTypeId,
|
||||
items: eventTypes
|
||||
.map((type) => DropdownMenuItem<String>(
|
||||
value: type.id,
|
||||
child: Text(type.name),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: onEventTypeChanged,
|
||||
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),
|
||||
_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,23 +24,43 @@ class EventFormActions extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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'),
|
||||
// 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,
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user