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.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- provider: true
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
|
|
||||||
class EventOption {
|
class EventOption {
|
||||||
final String id;
|
final String id;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class EventProvider with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isInWorkforce) {
|
if (isInWorkforce) {
|
||||||
print('Event ${event.name} includes user ${userId}');
|
print('Event ${event.name} includes user $userId');
|
||||||
}
|
}
|
||||||
|
|
||||||
return isInWorkforce;
|
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_staff_and_documents_section.dart';
|
||||||
import 'package:em2rp/views/widgets/event_form/event_form_actions.dart';
|
import 'package:em2rp/views/widgets/event_form/event_form_actions.dart';
|
||||||
import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
class EventAddEditPage extends StatefulWidget {
|
class EventAddEditPage extends StatefulWidget {
|
||||||
final EventModel? event;
|
final EventModel? event;
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_header.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_status_button.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_info.dart';
|
||||||
import 'package:em2rp/views/widgets/user_management/user_card.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_description.dart';
|
||||||
import 'package:em2rp/models/user_model.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart';
|
||||||
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_equipe.dart';
|
||||||
import 'package:em2rp/views/event_add_page.dart';
|
|
||||||
import 'package:em2rp/views/widgets/event_form/event_options_display_widget.dart';
|
|
||||||
|
|
||||||
class EventDetails extends StatelessWidget {
|
class EventDetails extends StatelessWidget {
|
||||||
final EventModel event;
|
final EventModel event;
|
||||||
@@ -31,15 +28,11 @@ class EventDetails extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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
|
// Trie les événements par date de début
|
||||||
final sortedEvents = List<EventModel>.from(events)
|
final sortedEvents = List<EventModel>.from(events)
|
||||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id);
|
final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id);
|
||||||
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||||
final isAdmin = localUserProvider.hasPermission('view_all_users');
|
|
||||||
final canViewPrices = localUserProvider.hasPermission('view_event_prices');
|
final canViewPrices = localUserProvider.hasPermission('view_event_prices');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
@@ -49,121 +42,22 @@ class EventDetails extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
EventDetailsNavigation(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
sortedEvents: sortedEvents,
|
||||||
children: [
|
currentIndex: currentIndex,
|
||||||
IconButton(
|
selectedDate: selectedDate,
|
||||||
onPressed: currentIndex > 0
|
onSelectEvent: onSelectEvent,
|
||||||
? () {
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
EventDetailsHeader(event: event),
|
||||||
//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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||||
.hasPermission('change_event_status'))
|
.hasPermission('change_event_status'))
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
child: _FirestoreStatusButton(
|
child: EventStatusButton(
|
||||||
eventId: event.id,
|
event: event,
|
||||||
currentStatus: event.status,
|
selectedDate: selectedDate,
|
||||||
onStatusChanged: (newStatus) async {
|
onSelectEvent: onSelectEvent,
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -172,214 +66,15 @@ class EventDetails extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow(
|
EventDetailsInfo(
|
||||||
context,
|
event: event,
|
||||||
Icons.calendar_today,
|
canViewPrices: canViewPrices,
|
||||||
'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, // 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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
EventDetailsDescription(event: event),
|
||||||
'Description',
|
EventDetailsDocuments(documents: event.documents),
|
||||||
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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
EventDetailsEquipe(workforce: event.workforce),
|
||||||
'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),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -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 {
|
class EventAddDialog extends StatefulWidget {
|
||||||
const EventAddDialog({super.key});
|
const EventAddDialog({super.key});
|
||||||
|
|
||||||
@@ -486,7 +126,9 @@ class _EventAddDialogState extends State<EventAddDialog> {
|
|||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_formKey.currentState!.validate() ||
|
if (!_formKey.currentState!.validate() ||
|
||||||
_startDateTime == null ||
|
_startDateTime == null ||
|
||||||
_endDateTime == null) return;
|
_endDateTime == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_error = null;
|
_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:flutter/material.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:em2rp/models/option_model.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:em2rp/utils/colors.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/models/user_model.dart';
|
||||||
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
||||||
import 'package:em2rp/views/widgets/inputs/dropzone_upload_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 {
|
class EventStaffAndDocumentsSection extends StatelessWidget {
|
||||||
final List<UserModel> allUsers;
|
final List<UserModel> allUsers;
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Debug : afficher le contenu envoyé
|
// 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({
|
await FirebaseFirestore.instance.collection('options').add({
|
||||||
'name': name,
|
'name': name,
|
||||||
'details': _detailsController.text.trim(),
|
'details': _detailsController.text.trim(),
|
||||||
@@ -574,7 +574,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
|||||||
});
|
});
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} catch (e) {
|
} 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
|
child: _checkingName
|
||||||
|
|||||||
Reference in New Issue
Block a user