split et refacto de event_details.dart
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- provider: true
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class EventOption {
|
||||
final String id;
|
||||
|
||||
@@ -58,7 +58,7 @@ class EventProvider with ChangeNotifier {
|
||||
});
|
||||
|
||||
if (isInWorkforce) {
|
||||
print('Event ${event.name} includes user ${userId}');
|
||||
print('Event ${event.name} includes user $userId');
|
||||
}
|
||||
|
||||
return isInWorkforce;
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:em2rp/views/widgets/event_form/event_details_section.dart';
|
||||
import 'package:em2rp/views/widgets/event_form/event_staff_and_documents_section.dart';
|
||||
import 'package:em2rp/views/widgets/event_form/event_form_actions.dart';
|
||||
import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class EventAddEditPage extends StatefulWidget {
|
||||
final EventModel? event;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
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';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_header.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_status_button.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_info.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_description.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart';
|
||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart';
|
||||
|
||||
class EventDetails extends StatelessWidget {
|
||||
final EventModel event;
|
||||
@@ -31,15 +28,11 @@ class EventDetails extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
|
||||
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '€');
|
||||
final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR');
|
||||
// Trie les événements par date de début
|
||||
final sortedEvents = List<EventModel>.from(events)
|
||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id);
|
||||
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||
final isAdmin = localUserProvider.hasPermission('view_all_users');
|
||||
final canViewPrices = localUserProvider.hasPermission('view_event_prices');
|
||||
|
||||
return Card(
|
||||
@@ -49,121 +42,22 @@ class EventDetails extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: currentIndex > 0
|
||||
? () {
|
||||
final prevEvent = sortedEvents[currentIndex - 1];
|
||||
onSelectEvent(prevEvent, prevEvent.startDateTime);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
if (selectedDate != null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
fullDateFormat.format(selectedDate!),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.rouge,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: currentIndex < sortedEvents.length - 1
|
||||
? () {
|
||||
final nextEvent = sortedEvents[currentIndex + 1];
|
||||
onSelectEvent(nextEvent, nextEvent.startDateTime);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
],
|
||||
EventDetailsNavigation(
|
||||
sortedEvents: sortedEvents,
|
||||
currentIndex: currentIndex,
|
||||
selectedDate: selectedDate,
|
||||
onSelectEvent: onSelectEvent,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
//Titre de l'événement
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
event.name,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: AppColors.noir,
|
||||
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(
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Statut de l'événement
|
||||
|
||||
const SizedBox(width: 8),
|
||||
Spacer(),
|
||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||
.hasPermission('edit_event'))
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: AppColors.rouge),
|
||||
tooltip: 'Modifier',
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EventAddEditPage(event: event),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
EventDetailsHeader(event: event),
|
||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||
.hasPermission('change_event_status'))
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: _FirestoreStatusButton(
|
||||
eventId: event.id,
|
||||
currentStatus: event.status,
|
||||
onStatusChanged: (newStatus) async {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(event.id)
|
||||
.update({'status': eventStatusToString(newStatus)});
|
||||
// Recharge l'événement depuis Firestore et notifie le parent
|
||||
final snap = await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(event.id)
|
||||
.get();
|
||||
final updatedEvent =
|
||||
EventModel.fromMap(snap.data()!, event.id);
|
||||
onSelectEvent(updatedEvent,
|
||||
selectedDate ?? updatedEvent.startDateTime);
|
||||
// Met à jour uniquement l'événement dans le provider (rafraîchissement local et fluide)
|
||||
await Provider.of<EventProvider>(context, listen: false)
|
||||
.updateEvent(updatedEvent);
|
||||
},
|
||||
child: EventStatusButton(
|
||||
event: event,
|
||||
selectedDate: selectedDate,
|
||||
onSelectEvent: onSelectEvent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -172,214 +66,15 @@ class EventDetails extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.calendar_today,
|
||||
'Horaire de début',
|
||||
dateFormat.format(event.startDateTime),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.calendar_today,
|
||||
'Horaire de fin',
|
||||
dateFormat.format(event.endDateTime),
|
||||
),
|
||||
if (canViewPrices)
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.euro,
|
||||
'Prix de base',
|
||||
currencyFormat.format(event.basePrice),
|
||||
),
|
||||
if (event.options.isNotEmpty) ...[
|
||||
EventOptionsDisplayWidget(
|
||||
optionsData: event.options,
|
||||
EventDetailsInfo(
|
||||
event: event,
|
||||
canViewPrices: canViewPrices,
|
||||
showPriceCalculation: false, // On affiche le total séparément
|
||||
),
|
||||
],
|
||||
if (canViewPrices) ...[
|
||||
const SizedBox(height: 4),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final total = event.basePrice +
|
||||
event.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.attach_money,
|
||||
color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text('Prix total : ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
Text(
|
||||
currencyFormat.format(total),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
color: AppColors.rouge,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
_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',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
event.description,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
EventDetailsDescription(event: event),
|
||||
EventDetailsDocuments(documents: event.documents),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Adresse',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
event.address,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (event.latitude != 0.0 || event.longitude != 0.0) ...[
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(
|
||||
'${event.latitude}° N, ${event.longitude}° E',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
if (event.documents.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text('Documents',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: event.documents.map((doc) {
|
||||
final fileName = doc['name'] ?? '';
|
||||
final url = doc['url'] ?? '';
|
||||
final ext = p.extension(fileName).toLowerCase();
|
||||
IconData icon;
|
||||
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]
|
||||
.contains(ext)) {
|
||||
icon = Icons.image;
|
||||
} else if (ext == ".pdf") {
|
||||
icon = Icons.picture_as_pdf;
|
||||
} else if ([
|
||||
".txt",
|
||||
".md",
|
||||
".csv",
|
||||
".json",
|
||||
".xml",
|
||||
".docx",
|
||||
".doc",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx"
|
||||
].contains(ext)) {
|
||||
icon = Icons.description;
|
||||
} else {
|
||||
icon = Icons.attach_file;
|
||||
}
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Colors.blueGrey),
|
||||
title: SelectableText(
|
||||
fileName,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
// --- EQUIPE SECTION ---
|
||||
const SizedBox(height: 16),
|
||||
EquipeSection(workforce: event.workforce),
|
||||
],
|
||||
EventDetailsEquipe(workforce: event.workforce),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -389,64 +84,9 @@ class EventDetails extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label : ',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(EventStatus status) {
|
||||
Color color;
|
||||
IconData icon;
|
||||
String tooltip;
|
||||
switch (status) {
|
||||
case EventStatus.confirmed:
|
||||
color = Colors.green;
|
||||
icon = Icons.check_circle;
|
||||
tooltip = 'Confirmé';
|
||||
break;
|
||||
case EventStatus.canceled:
|
||||
color = Colors.red;
|
||||
icon = Icons.cancel;
|
||||
tooltip = 'Annulé';
|
||||
break;
|
||||
case EventStatus.waitingForApproval:
|
||||
default:
|
||||
color = Colors.amber;
|
||||
icon = Icons.hourglass_empty;
|
||||
tooltip = 'En attente de validation';
|
||||
break;
|
||||
}
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Icon(icon, color: color, size: 28),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// La classe EventAddDialog reste inchangée car elle n'est pas liée aux détails d'événement
|
||||
class EventAddDialog extends StatefulWidget {
|
||||
const EventAddDialog({super.key});
|
||||
|
||||
@@ -486,7 +126,9 @@ class _EventAddDialogState extends State<EventAddDialog> {
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate() ||
|
||||
_startDateTime == null ||
|
||||
_endDateTime == null) return;
|
||||
_endDateTime == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
@@ -685,214 +327,3 @@ class _EventAddDialogState extends State<EventAddDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FirestoreStatusButton extends StatefulWidget {
|
||||
final String eventId;
|
||||
final EventStatus currentStatus;
|
||||
final Future<void> Function(EventStatus) onStatusChanged;
|
||||
const _FirestoreStatusButton({
|
||||
required this.eventId,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_FirestoreStatusButton> createState() => _FirestoreStatusButtonState();
|
||||
}
|
||||
|
||||
class _FirestoreStatusButtonState extends State<_FirestoreStatusButton> {
|
||||
bool _loading = false;
|
||||
|
||||
Future<void> changerStatut(EventStatus nouveau) async {
|
||||
if (widget.currentStatus == nouveau) return;
|
||||
setState(() => _loading = true);
|
||||
await widget.onStatusChanged(nouveau);
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _FirestoreStatusButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Si l'événement change, on arrête le loading (sécurité UX)
|
||||
if (oldWidget.eventId != widget.eventId ||
|
||||
oldWidget.currentStatus != widget.currentStatus) {
|
||||
if (_loading) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = widget.currentStatus;
|
||||
String texte;
|
||||
Color couleurFond;
|
||||
List<Widget> enfants = [];
|
||||
switch (status) {
|
||||
case EventStatus.waitingForApproval:
|
||||
texte = "En Attente";
|
||||
couleurFond = Colors.yellow.shade600;
|
||||
enfants = [
|
||||
_buildIconButton(Icons.close, Colors.red,
|
||||
() => changerStatut(EventStatus.canceled)),
|
||||
_buildLabel(texte, couleurFond),
|
||||
_buildIconButton(Icons.check, Colors.green,
|
||||
() => changerStatut(EventStatus.confirmed)),
|
||||
];
|
||||
break;
|
||||
case EventStatus.confirmed:
|
||||
texte = "Confirmé";
|
||||
couleurFond = Colors.green;
|
||||
enfants = [
|
||||
_buildIconButton(Icons.close, Colors.red,
|
||||
() => changerStatut(EventStatus.canceled)),
|
||||
_buildIconButton(Icons.hourglass_empty, Colors.yellow.shade700,
|
||||
() => changerStatut(EventStatus.waitingForApproval)),
|
||||
_buildLabel(texte, couleurFond),
|
||||
];
|
||||
break;
|
||||
case EventStatus.canceled:
|
||||
texte = "Annulé";
|
||||
couleurFond = Colors.red;
|
||||
enfants = [
|
||||
_buildLabel(texte, couleurFond),
|
||||
_buildIconButton(Icons.hourglass_empty, Colors.yellow.shade700,
|
||||
() => changerStatut(EventStatus.waitingForApproval)),
|
||||
_buildIconButton(Icons.check, Colors.green,
|
||||
() => changerStatut(EventStatus.confirmed)),
|
||||
];
|
||||
break;
|
||||
}
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: enfants,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLabel(String texte, Color couleur) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: couleur,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
texte,
|
||||
key: ValueKey(texte),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.white, fontSize: 13),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconButton(
|
||||
IconData icone, Color couleur, VoidCallback onPressed) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: couleur, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icone, color: couleur, size: 16),
|
||||
onPressed: _loading ? null : onPressed,
|
||||
splashRadius: 16,
|
||||
tooltip: 'Changer statut',
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EquipeSection extends StatelessWidget {
|
||||
final List workforce;
|
||||
const EquipeSection({super.key, required this.workforce});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (workforce.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
Text('Aucun membre assigné.',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
return FutureBuilder<List<UserModel>>(
|
||||
future: _fetchUsers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
snapshot.error.toString().contains('permission-denied')
|
||||
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
|
||||
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
final users = snapshot.data ?? [];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
if (users.isEmpty)
|
||||
Text('Aucun membre assigné.',
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
if (users.isNotEmpty)
|
||||
UserChipsList(
|
||||
users: users,
|
||||
showRemove: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<UserModel>> _fetchUsers() async {
|
||||
final firestore = FirebaseFirestore.instance;
|
||||
List<UserModel> users = [];
|
||||
for (final ref in workforce) {
|
||||
try {
|
||||
final doc = await firestore.doc(ref.path).get();
|
||||
if (doc.exists) {
|
||||
users.add(
|
||||
UserModel.fromMap(doc.data() as Map<String, dynamic>, doc.id));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
class EventDetailsDescription extends StatelessWidget {
|
||||
final EventModel event;
|
||||
|
||||
const EventDetailsDescription({
|
||||
super.key,
|
||||
required this.event,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Description',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
event.description,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Adresse',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
event.address,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (event.latitude != 0.0 || event.longitude != 0.0) ...[
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(
|
||||
'${event.latitude}° N, ${event.longitude}° E',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class EventDetailsDocuments extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> documents;
|
||||
|
||||
const EventDetailsDocuments({
|
||||
super.key,
|
||||
required this.documents,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (documents.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Documents',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: documents.map((doc) {
|
||||
final fileName = doc['name'] ?? '';
|
||||
final url = doc['url'] ?? '';
|
||||
final ext = p.extension(fileName).toLowerCase();
|
||||
IconData icon;
|
||||
|
||||
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]
|
||||
.contains(ext)) {
|
||||
icon = Icons.image;
|
||||
} else if (ext == ".pdf") {
|
||||
icon = Icons.picture_as_pdf;
|
||||
} else if ([
|
||||
".txt",
|
||||
".md",
|
||||
".csv",
|
||||
".json",
|
||||
".xml",
|
||||
".docx",
|
||||
".doc",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx"
|
||||
].contains(ext)) {
|
||||
icon = Icons.description;
|
||||
} else {
|
||||
icon = Icons.attach_file;
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Colors.blueGrey),
|
||||
title: SelectableText(
|
||||
fileName,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
||||
|
||||
class EventDetailsEquipe extends StatelessWidget {
|
||||
final List workforce;
|
||||
|
||||
const EventDetailsEquipe({
|
||||
super.key,
|
||||
required this.workforce,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (workforce.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucun membre assigné.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder<List<UserModel>>(
|
||||
future: _fetchUsers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
snapshot.error.toString().contains('permission-denied')
|
||||
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
|
||||
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final users = snapshot.data ?? [];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Equipe',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (users.isEmpty)
|
||||
Text(
|
||||
'Aucun membre assigné ou erreur de chargement.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
if (users.isNotEmpty)
|
||||
UserChipsList(
|
||||
users: users,
|
||||
showRemove: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<UserModel>> _fetchUsers() async {
|
||||
final firestore = FirebaseFirestore.instance;
|
||||
List<UserModel> users = [];
|
||||
|
||||
for (int i = 0; i < workforce.length; i++) {
|
||||
final ref = workforce[i];
|
||||
try {
|
||||
if (ref is DocumentReference) {
|
||||
final doc = await firestore.doc(ref.path).get();
|
||||
if (doc.exists) {
|
||||
final userData = doc.data() as Map<String, dynamic>;
|
||||
users.add(UserModel.fromMap(userData, doc.id));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Log silencieux des erreurs individuelles
|
||||
debugPrint('Error fetching user $i: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/views/event_add_page.dart';
|
||||
|
||||
class EventDetailsHeader extends StatelessWidget {
|
||||
final EventModel event;
|
||||
|
||||
const EventDetailsHeader({
|
||||
super.key,
|
||||
required this.event,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 80),
|
||||
child: SelectableText(
|
||||
event.name,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
event.eventTypeId,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusIcon(event.status),
|
||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||
.hasPermission('edit_event')) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: AppColors.rouge),
|
||||
tooltip: 'Modifier',
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EventAddEditPage(event: event),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(EventStatus status) {
|
||||
Color color;
|
||||
IconData icon;
|
||||
String tooltip;
|
||||
switch (status) {
|
||||
case EventStatus.confirmed:
|
||||
color = Colors.green;
|
||||
icon = Icons.check_circle;
|
||||
tooltip = 'Confirmé';
|
||||
break;
|
||||
case EventStatus.canceled:
|
||||
color = Colors.red;
|
||||
icon = Icons.cancel;
|
||||
tooltip = 'Annulé';
|
||||
break;
|
||||
case EventStatus.waitingForApproval:
|
||||
color = Colors.amber;
|
||||
icon = Icons.hourglass_empty;
|
||||
tooltip = 'En attente de validation';
|
||||
break;
|
||||
}
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Icon(icon, color: color, size: 28),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:em2rp/views/widgets/event_form/event_options_display_widget.dart';
|
||||
|
||||
class EventDetailsInfo extends StatelessWidget {
|
||||
final EventModel event;
|
||||
final bool canViewPrices;
|
||||
|
||||
const EventDetailsInfo({
|
||||
super.key,
|
||||
required this.event,
|
||||
required this.canViewPrices,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
|
||||
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '€');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.calendar_today,
|
||||
'Horaire de début',
|
||||
dateFormat.format(event.startDateTime),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.calendar_today,
|
||||
'Horaire de fin',
|
||||
dateFormat.format(event.endDateTime),
|
||||
),
|
||||
if (canViewPrices)
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.euro,
|
||||
'Prix de base',
|
||||
currencyFormat.format(event.basePrice),
|
||||
),
|
||||
if (event.options.isNotEmpty) ...[
|
||||
EventOptionsDisplayWidget(
|
||||
optionsData: event.options,
|
||||
canViewPrices: canViewPrices,
|
||||
showPriceCalculation: false,
|
||||
),
|
||||
],
|
||||
if (canViewPrices) ...[
|
||||
const SizedBox(height: 4),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final total = event.basePrice +
|
||||
event.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.attach_money, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Prix total : ',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
currencyFormat.format(total),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: AppColors.rouge,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.build,
|
||||
'Temps d\'installation',
|
||||
'${event.installationTime} heures',
|
||||
),
|
||||
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.format(arrival)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
Icons.construction,
|
||||
'Temps de démontage',
|
||||
'${event.disassemblyTime} heures',
|
||||
),
|
||||
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.format(departure)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label : ',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: AppColors.noir,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class EventDetailsNavigation extends StatelessWidget {
|
||||
final List<EventModel> sortedEvents;
|
||||
final int currentIndex;
|
||||
final DateTime? selectedDate;
|
||||
final void Function(EventModel, DateTime) onSelectEvent;
|
||||
|
||||
const EventDetailsNavigation({
|
||||
super.key,
|
||||
required this.sortedEvents,
|
||||
required this.currentIndex,
|
||||
required this.selectedDate,
|
||||
required this.onSelectEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR');
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: currentIndex > 0
|
||||
? () {
|
||||
final prevEvent = sortedEvents[currentIndex - 1];
|
||||
onSelectEvent(prevEvent, prevEvent.startDateTime);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
if (selectedDate != null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
fullDateFormat.format(selectedDate!),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.rouge,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: currentIndex < sortedEvents.length - 1
|
||||
? () {
|
||||
final nextEvent = sortedEvents[currentIndex + 1];
|
||||
onSelectEvent(nextEvent, nextEvent.startDateTime);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
|
||||
class EventStatusButton extends StatefulWidget {
|
||||
final EventModel event;
|
||||
final DateTime? selectedDate;
|
||||
final void Function(EventModel, DateTime) onSelectEvent;
|
||||
|
||||
const EventStatusButton({
|
||||
super.key,
|
||||
required this.event,
|
||||
required this.selectedDate,
|
||||
required this.onSelectEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EventStatusButton> createState() => _EventStatusButtonState();
|
||||
}
|
||||
|
||||
class _EventStatusButtonState extends State<EventStatusButton> {
|
||||
bool _loading = false;
|
||||
|
||||
Future<void> _changeStatus(EventStatus newStatus) async {
|
||||
if (widget.event.status == newStatus) return;
|
||||
setState(() => _loading = true);
|
||||
|
||||
try {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(widget.event.id)
|
||||
.update({'status': eventStatusToString(newStatus)});
|
||||
|
||||
final snap = await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(widget.event.id)
|
||||
.get();
|
||||
final updatedEvent = EventModel.fromMap(snap.data()!, widget.event.id);
|
||||
|
||||
widget.onSelectEvent(
|
||||
updatedEvent,
|
||||
widget.selectedDate ?? updatedEvent.startDateTime,
|
||||
);
|
||||
|
||||
await Provider.of<EventProvider>(context, listen: false)
|
||||
.updateEvent(updatedEvent);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur lors du changement de statut: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = widget.event.status;
|
||||
String texte;
|
||||
Color couleurFond;
|
||||
List<Widget> enfants = [];
|
||||
|
||||
switch (status) {
|
||||
case EventStatus.waitingForApproval:
|
||||
texte = "En Attente";
|
||||
couleurFond = Colors.yellow.shade600;
|
||||
enfants = [
|
||||
_buildIconButton(
|
||||
Icons.close,
|
||||
Colors.red,
|
||||
() => _changeStatus(EventStatus.canceled),
|
||||
),
|
||||
_buildLabel(texte, couleurFond),
|
||||
_buildIconButton(
|
||||
Icons.check,
|
||||
Colors.green,
|
||||
() => _changeStatus(EventStatus.confirmed),
|
||||
),
|
||||
];
|
||||
break;
|
||||
case EventStatus.confirmed:
|
||||
texte = "Confirmé";
|
||||
couleurFond = Colors.green;
|
||||
enfants = [
|
||||
_buildIconButton(
|
||||
Icons.close,
|
||||
Colors.red,
|
||||
() => _changeStatus(EventStatus.canceled),
|
||||
),
|
||||
_buildIconButton(
|
||||
Icons.hourglass_empty,
|
||||
Colors.yellow.shade700,
|
||||
() => _changeStatus(EventStatus.waitingForApproval),
|
||||
),
|
||||
_buildLabel(texte, couleurFond),
|
||||
];
|
||||
break;
|
||||
case EventStatus.canceled:
|
||||
texte = "Annulé";
|
||||
couleurFond = Colors.red;
|
||||
enfants = [
|
||||
_buildLabel(texte, couleurFond),
|
||||
_buildIconButton(
|
||||
Icons.hourglass_empty,
|
||||
Colors.yellow.shade700,
|
||||
() => _changeStatus(EventStatus.waitingForApproval),
|
||||
),
|
||||
_buildIconButton(
|
||||
Icons.check,
|
||||
Colors.green,
|
||||
() => _changeStatus(EventStatus.confirmed),
|
||||
),
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: enfants,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLabel(String texte, Color couleur) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: couleur,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
texte,
|
||||
key: ValueKey(texte),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconButton(
|
||||
IconData icone,
|
||||
Color couleur,
|
||||
VoidCallback onPressed,
|
||||
) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: couleur, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icone, color: couleur, size: 16),
|
||||
onPressed: _loading ? null : onPressed,
|
||||
splashRadius: 16,
|
||||
tooltip: 'Changer statut',
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import 'package:flutter/material.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/inputs/dropzone_upload_widget.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
|
||||
class EventStaffAndDocumentsSection extends StatelessWidget {
|
||||
final List<UserModel> allUsers;
|
||||
|
||||
@@ -564,7 +564,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
}
|
||||
try {
|
||||
// Debug : afficher le contenu envoyé
|
||||
print('Enregistrement option avec eventTypes : [32m[1m[4m[7m' + _selectedTypes.toString() + '\u001b[0m');
|
||||
print('Enregistrement option avec eventTypes : $_selectedTypes\u001b');
|
||||
await FirebaseFirestore.instance.collection('options').add({
|
||||
'name': name,
|
||||
'details': _detailsController.text.trim(),
|
||||
@@ -574,7 +574,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
});
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
setState(() => _error = 'Erreur lors de la création : ' + e.toString() + '\nEventTypes=' + _selectedTypes.toString());
|
||||
setState(() => _error = 'Erreur lors de la création : $e\nEventTypes=$_selectedTypes');
|
||||
}
|
||||
},
|
||||
child: _checkingName
|
||||
|
||||
Reference in New Issue
Block a user