Files
EM2_ERP/em2rp/lib/views/widgets/calendar_widgets/event_details.dart

757 lines
28 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';
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');
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),
],
),
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)});
},
),
),
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),
),
_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: Text(
(isNegative ? '- ' : '+ ') +
currencyFormat.format(price.abs()),
style: TextStyle(
color: isNegative ? Colors.red : AppColors.noir,
fontWeight: FontWeight.bold,
),
),
contentPadding: EdgeInsets.zero,
dense: true,
);
}).toList(),
),
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(),
),
],
],
),
),
),
],
),
),
);
}
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> {
late EventStatus _status;
bool _loading = false;
@override
void initState() {
super.initState();
_status = widget.currentStatus;
}
Future<void> changerStatut(EventStatus nouveau) async {
if (_status == nouveau) return;
setState(() => _loading = true);
await FirebaseFirestore.instance
.collection('events')
.doc(widget.eventId)
.update({'status': eventStatusToString(nouveau)});
setState(() {
_status = nouveau;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
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),
),
);
}
}