874 lines
33 KiB
Dart
874 lines
33 KiB
Dart
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';
|
|
|
|
class EventDetails extends StatelessWidget {
|
|
final EventModel event;
|
|
final DateTime? selectedDate;
|
|
final List<EventModel> events;
|
|
final void Function(EventModel, DateTime) onSelectEvent;
|
|
|
|
const EventDetails({
|
|
super.key,
|
|
required this.event,
|
|
required this.selectedDate,
|
|
required this.events,
|
|
required this.onSelectEvent,
|
|
});
|
|
|
|
@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(
|
|
margin: const EdgeInsets.all(16),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
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(width: 8),
|
|
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)
|
|
.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);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildInfoRow(
|
|
context,
|
|
Icons.calendar_today,
|
|
'Date de début',
|
|
dateFormat.format(event.startDateTime),
|
|
),
|
|
_buildInfoRow(
|
|
context,
|
|
Icons.calendar_today,
|
|
'Date de fin',
|
|
dateFormat.format(event.endDateTime),
|
|
),
|
|
if (canViewPrices)
|
|
_buildInfoRow(
|
|
context,
|
|
Icons.euro,
|
|
'Prix de base',
|
|
currencyFormat.format(event.basePrice),
|
|
),
|
|
if (event.options.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Text('Options sélectionnées',
|
|
style:
|
|
Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
color: AppColors.noir,
|
|
fontWeight: FontWeight.bold,
|
|
)),
|
|
const SizedBox(height: 4),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: event.options.map((opt) {
|
|
final price = (opt['price'] ?? 0.0) as num;
|
|
final isNegative = price < 0;
|
|
return ListTile(
|
|
leading: Icon(Icons.tune,
|
|
color:
|
|
isNegative ? Colors.red : AppColors.rouge),
|
|
title: Text(opt['name'] ?? '',
|
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
|
subtitle: Text(opt['details'] ?? ''),
|
|
trailing: canViewPrices
|
|
? Text(
|
|
(isNegative ? '- ' : '+ ') +
|
|
currencyFormat.format(price.abs()),
|
|
style: TextStyle(
|
|
color: isNegative
|
|
? Colors.red
|
|
: AppColors.noir,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
)
|
|
: null,
|
|
contentPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
);
|
|
}).toList(),
|
|
),
|
|
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',
|
|
),
|
|
_buildInfoRow(
|
|
context,
|
|
Icons.construction,
|
|
'Temps de démontage',
|
|
'${event.disassemblyTime} heures',
|
|
),
|
|
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,
|
|
),
|
|
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),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EventAddDialog extends StatefulWidget {
|
|
const EventAddDialog({super.key});
|
|
|
|
@override
|
|
State<EventAddDialog> createState() => _EventAddDialogState();
|
|
}
|
|
|
|
class _EventAddDialogState extends State<EventAddDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final TextEditingController _nameController = TextEditingController();
|
|
final TextEditingController _descriptionController = TextEditingController();
|
|
final TextEditingController _priceController = TextEditingController();
|
|
final TextEditingController _installationController = TextEditingController();
|
|
final TextEditingController _disassemblyController = TextEditingController();
|
|
final TextEditingController _latitudeController = TextEditingController();
|
|
final TextEditingController _longitudeController = TextEditingController();
|
|
final TextEditingController _addressController = TextEditingController();
|
|
DateTime? _startDateTime;
|
|
DateTime? _endDateTime;
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
String? _success;
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_descriptionController.dispose();
|
|
_priceController.dispose();
|
|
_installationController.dispose();
|
|
_disassemblyController.dispose();
|
|
_latitudeController.dispose();
|
|
_longitudeController.dispose();
|
|
_addressController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (!_formKey.currentState!.validate() ||
|
|
_startDateTime == null ||
|
|
_endDateTime == null) return;
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
_success = null;
|
|
});
|
|
try {
|
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
|
final newEvent = EventModel(
|
|
id: '',
|
|
name: _nameController.text.trim(),
|
|
description: _descriptionController.text.trim(),
|
|
startDateTime: _startDateTime!,
|
|
endDateTime: _endDateTime!,
|
|
basePrice: double.tryParse(_priceController.text) ?? 0.0,
|
|
installationTime: int.tryParse(_installationController.text) ?? 0,
|
|
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
|
|
eventTypeId: '', // à adapter si tu veux gérer les types
|
|
customerId: '', // à adapter si tu veux gérer les clients
|
|
address: _addressController.text.trim(),
|
|
latitude: double.tryParse(_latitudeController.text) ?? 0.0,
|
|
longitude: double.tryParse(_longitudeController.text) ?? 0.0,
|
|
workforce: [],
|
|
documents: [],
|
|
);
|
|
await eventProvider.addEvent(newEvent);
|
|
setState(() {
|
|
_success = "Événement créé avec succès !";
|
|
});
|
|
Navigator.of(context).pop();
|
|
} catch (e) {
|
|
setState(() {
|
|
_error = "Erreur lors de la création : $e";
|
|
});
|
|
} finally {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Créer un événement'),
|
|
content: SingleChildScrollView(
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
controller: _nameController,
|
|
decoration: const InputDecoration(labelText: 'Nom'),
|
|
validator: (v) =>
|
|
v == null || v.isEmpty ? 'Champ requis' : null,
|
|
),
|
|
TextFormField(
|
|
controller: _descriptionController,
|
|
decoration: const InputDecoration(labelText: 'Description'),
|
|
maxLines: 2,
|
|
),
|
|
TextFormField(
|
|
controller: _priceController,
|
|
decoration: const InputDecoration(labelText: 'Prix (€)'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
TextFormField(
|
|
controller: _installationController,
|
|
decoration:
|
|
const InputDecoration(labelText: 'Installation (h)'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
TextFormField(
|
|
controller: _disassemblyController,
|
|
decoration: const InputDecoration(labelText: 'Démontage (h)'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
TextFormField(
|
|
controller: _latitudeController,
|
|
decoration: const InputDecoration(labelText: 'Latitude'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
TextFormField(
|
|
controller: _longitudeController,
|
|
decoration: const InputDecoration(labelText: 'Longitude'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
TextFormField(
|
|
controller: _addressController,
|
|
decoration: const InputDecoration(labelText: 'Adresse'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () async {
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: DateTime.now(),
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime(2030),
|
|
);
|
|
if (picked != null) {
|
|
final time = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.now(),
|
|
);
|
|
if (time != null) {
|
|
setState(() {
|
|
_startDateTime = DateTime(
|
|
picked.year,
|
|
picked.month,
|
|
picked.day,
|
|
time.hour,
|
|
time.minute,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
child: Text(_startDateTime == null
|
|
? 'Début'
|
|
: DateFormat('dd/MM/yyyy HH:mm')
|
|
.format(_startDateTime!)),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () async {
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _startDateTime ?? DateTime.now(),
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime(2030),
|
|
);
|
|
if (picked != null) {
|
|
final time = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.now(),
|
|
);
|
|
if (time != null) {
|
|
setState(() {
|
|
_endDateTime = DateTime(
|
|
picked.year,
|
|
picked.month,
|
|
picked.day,
|
|
time.hour,
|
|
time.minute,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
child: Text(_endDateTime == null
|
|
? 'Fin'
|
|
: DateFormat('dd/MM/yyyy HH:mm')
|
|
.format(_endDateTime!)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (_error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child:
|
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
|
),
|
|
if (_success != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(_success!,
|
|
style: const TextStyle(color: Colors.green)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _isLoading ? null : _submit,
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Text('Créer'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
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('Erreur lors du chargement de l\'équipe',
|
|
style: 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;
|
|
}
|
|
}
|