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

@@ -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),

View File

@@ -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),
),

View File

@@ -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'),
),
],
),
],
),

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;
}
}