Equipe sur event details OK

Modif evenement OK
This commit is contained in:
2025-06-03 19:59:40 +02:00
parent acab16e101
commit 57c59c911a
6 changed files with 299 additions and 111 deletions

View File

@ -145,7 +145,7 @@ class _CalendarPageState extends State<CalendarPage> {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const EventAddPage(),
builder: (context) => const EventAddEditPage(),
),
);
},

View File

@ -23,14 +23,15 @@ import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
class EventAddPage extends StatefulWidget {
const EventAddPage({super.key});
class EventAddEditPage extends StatefulWidget {
final EventModel? event;
const EventAddEditPage({super.key, this.event});
@override
State<EventAddPage> createState() => _EventAddPageState();
State<EventAddEditPage> createState() => _EventAddEditPageState();
}
class _EventAddPageState extends State<EventAddPage> {
class _EventAddEditPageState extends State<EventAddEditPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
@ -61,6 +62,8 @@ class _EventAddPageState extends State<EventAddPage> {
bool _formChanged = false;
EventStatus _selectedStatus = EventStatus.waitingForApproval;
bool get isEditMode => widget.event != null;
@override
void initState() {
super.initState();
@ -73,7 +76,24 @@ class _EventAddPageState extends State<EventAddPage> {
_addressController.addListener(_onAnyFieldChanged);
_descriptionController.addListener(_onAnyFieldChanged);
_addBeforeUnloadListener();
_selectedStatus = EventStatus.waitingForApproval;
if (isEditMode) {
final e = widget.event!;
_nameController.text = e.name;
_descriptionController.text = e.description;
_basePriceController.text = e.basePrice.toStringAsFixed(2);
_installationController.text = e.installationTime.toString();
_disassemblyController.text = e.disassemblyTime.toString();
_addressController.text = e.address;
_startDateTime = e.startDateTime;
_endDateTime = e.endDateTime;
_selectedEventType = e.eventTypeId.isNotEmpty ? e.eventTypeId : null;
_selectedUserIds = e.workforce.map((ref) => ref.id).toList();
_uploadedFiles = List<Map<String, String>>.from(e.documents);
_selectedOptions = List<Map<String, dynamic>>.from(e.options);
_selectedStatus = e.status;
} else {
_selectedStatus = EventStatus.waitingForApproval;
}
}
void _handleDescriptionChange() {
@ -284,78 +304,121 @@ class _EventAddPageState extends State<EventAddPage> {
});
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(_basePriceController.text) ?? 0.0,
installationTime: int.tryParse(_installationController.text) ?? 0,
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
eventTypeId: _selectedEventType!,
customerId: '',
address: _addressController.text.trim(),
workforce: _selectedUserIds
.map((id) => FirebaseFirestore.instance.collection('users').doc(id))
.toList(),
latitude: 0.0,
longitude: 0.0,
documents: _uploadedFiles,
options: _selectedOptions
.map((opt) => {
'name': opt['name'],
'price': opt['price'],
})
.toList(),
status: _selectedStatus,
);
final docRef = await FirebaseFirestore.instance
.collection('events')
.add(newEvent.toMap());
final eventId = docRef.id;
List<Map<String, String>> newFiles = [];
for (final file in _uploadedFiles) {
final fileName = file['name']!;
final oldUrl = file['url']!;
String sourcePath;
final tempPattern = RegExp(r'events/temp/[^?]+');
final match = tempPattern.firstMatch(oldUrl);
if (match != null) {
sourcePath = match.group(0)!;
} else {
final tempFileName =
Uri.decodeComponent(oldUrl.split('/').last.split('?').first);
sourcePath = tempFileName;
}
final destinationPath = 'events/$eventId/$fileName';
final newUrl = await moveEventFileHttp(
sourcePath: sourcePath,
destinationPath: destinationPath,
if (isEditMode) {
// Edition : on met à jour l'événement existant
final updatedEvent = EventModel(
id: widget.event!.id,
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
startDateTime: _startDateTime!,
endDateTime: _endDateTime!,
basePrice: double.tryParse(_basePriceController.text) ?? 0.0,
installationTime: int.tryParse(_installationController.text) ?? 0,
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
eventTypeId: _selectedEventType!,
customerId: '',
address: _addressController.text.trim(),
workforce: _selectedUserIds
.map((id) =>
FirebaseFirestore.instance.collection('users').doc(id))
.toList(),
latitude: 0.0,
longitude: 0.0,
documents: _uploadedFiles,
options: _selectedOptions
.map((opt) => {
'name': opt['name'],
'price': opt['price'],
})
.toList(),
status: _selectedStatus,
);
if (newUrl != null) {
newFiles.add({'name': fileName, 'url': newUrl});
} else {
newFiles.add({'name': fileName, 'url': oldUrl});
final docRef = FirebaseFirestore.instance
.collection('events')
.doc(widget.event!.id);
await docRef.update(updatedEvent.toMap());
// Gestion des fichiers (si besoin, à adapter selon ta logique)
// ...
setState(() {
_success = "Événement modifié avec succès !";
});
if (context.mounted) Navigator.of(context).pop();
} else {
// Création : logique existante
final newEvent = EventModel(
id: '',
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
startDateTime: _startDateTime!,
endDateTime: _endDateTime!,
basePrice: double.tryParse(_basePriceController.text) ?? 0.0,
installationTime: int.tryParse(_installationController.text) ?? 0,
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
eventTypeId: _selectedEventType!,
customerId: '',
address: _addressController.text.trim(),
workforce: _selectedUserIds
.map((id) =>
FirebaseFirestore.instance.collection('users').doc(id))
.toList(),
latitude: 0.0,
longitude: 0.0,
documents: _uploadedFiles,
options: _selectedOptions
.map((opt) => {
'name': opt['name'],
'price': opt['price'],
})
.toList(),
status: _selectedStatus,
);
final docRef = await FirebaseFirestore.instance
.collection('events')
.add(newEvent.toMap());
final eventId = docRef.id;
List<Map<String, String>> newFiles = [];
for (final file in _uploadedFiles) {
final fileName = file['name']!;
final oldUrl = file['url']!;
String sourcePath;
final tempPattern = RegExp(r'events/temp/[^?]+');
final match = tempPattern.firstMatch(oldUrl);
if (match != null) {
sourcePath = match.group(0)!;
} else {
final tempFileName =
Uri.decodeComponent(oldUrl.split('/').last.split('?').first);
sourcePath = tempFileName;
}
final destinationPath = 'events/$eventId/$fileName';
final newUrl = await moveEventFileHttp(
sourcePath: sourcePath,
destinationPath: destinationPath,
);
if (newUrl != null) {
newFiles.add({'name': fileName, 'url': newUrl});
} else {
newFiles.add({'name': fileName, 'url': oldUrl});
}
}
await docRef.update({'documents': newFiles});
final localUserProvider =
Provider.of<LocalUserProvider>(context, listen: false);
final userId = localUserProvider.uid;
final canViewAllEvents =
localUserProvider.hasPermission('view_all_events');
if (userId != null) {
await eventProvider.loadUserEvents(userId,
canViewAllEvents: canViewAllEvents);
}
setState(() {
_success = "Événement créé avec succès !";
});
if (context.mounted) Navigator.of(context).pop();
}
await docRef.update({'documents': newFiles});
final localUserProvider =
Provider.of<LocalUserProvider>(context, listen: false);
final userId = localUserProvider.uid;
final canViewAllEvents =
localUserProvider.hasPermission('view_all_events');
if (userId != null) {
await eventProvider.loadUserEvents(userId,
canViewAllEvents: canViewAllEvents);
}
setState(() {
_success = "Événement créé avec succès !";
});
if (context.mounted) Navigator.of(context).pop();
} catch (e) {
setState(() {
_error = "Erreur lors de la création : $e";
_error = "Erreur lors de la sauvegarde : $e";
});
} finally {
setState(() {
@ -385,7 +448,8 @@ class _EventAddPageState extends State<EventAddPage> {
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: const Text('Créer un événement'),
title:
Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'),
),
body: Center(
child: SingleChildScrollView(
@ -393,10 +457,7 @@ class _EventAddPageState extends State<EventAddPage> {
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
child: Container(
// Pas de Card sur mobile, juste un conteneur
child: _buildFormContent(isMobile),
),
child: _buildFormContent(isMobile),
)
: Card(
elevation: 6,
@ -764,23 +825,23 @@ class _EventAddPageState extends State<EventAddPage> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Créer'),
: Text(isEditMode ? 'Enregistrer' : 'Créer'),
),
],
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
icon: const Icon(Icons.check_circle, color: Colors.white),
label: const Text('Définir cet événement comme confirmé'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontWeight: FontWeight.bold),
if (!isEditMode)
Center(
child: ElevatedButton.icon(
icon: const Icon(Icons.check_circle, color: Colors.white),
label: const Text('Définir cet événement comme confirmé'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontWeight: FontWeight.bold),
),
onPressed: null,
),
onPressed: null,
),
),
],
),
);

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class ResetPasswordPage extends StatefulWidget {
final String email;
final String actionCode;
const ResetPasswordPage({
super.key,
required this.email,
required this.actionCode,
});
@override
ResetPasswordPageState createState() => ResetPasswordPageState();
}
class ResetPasswordPageState extends State<ResetPasswordPage> {
final _formKey = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
String? _errorMessage;
@override
void dispose() {
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _resetPassword() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Vérifier le code d'action
await FirebaseAuth.instance.verifyPasswordResetCode(widget.actionCode);
// Réinitialiser le mot de passe
await FirebaseAuth.instance.confirmPasswordReset(
code: widget.actionCode,
newPassword: _passwordController.text,
);
// Connecter l'utilisateur automatiquement
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: widget.email,
password: _passwordController.text,
);
if (mounted) {
Navigator.of(context).pushReplacementNamed('/home');
}
} catch (e) {
setState(() {
_errorMessage = 'Une erreur est survenue. Veuillez réessayer.';
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Créer votre mot de passe'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Bienvenue ! Veuillez créer votre mot de passe pour continuer.',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Nouveau mot de passe',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
decoration: const InputDecoration(
labelText: 'Confirmer le mot de passe',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
},
),
if (_errorMessage != null) ...[
const SizedBox(height: 16),
Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _resetPassword,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Créer le mot de passe'),
),
],
),
),
),
);
}
}

View File

@ -9,6 +9,10 @@ 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;
@ -93,6 +97,20 @@ class EventDetails extends StatelessWidget {
),
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)
@ -337,6 +355,9 @@ class EventDetails extends StatelessWidget {
);
}).toList(),
),
// --- EQUIPE SECTION ---
const SizedBox(height: 16),
EquipeSection(workforce: event.workforce),
],
],
),
@ -773,3 +794,80 @@ class _FirestoreStatusButtonState extends State<_FirestoreStatusButton> {
);
}
}
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;
}
}

View File

@ -70,26 +70,15 @@ class _UserMultiSelectState extends State<_UserMultiSelect> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 12,
runSpacing: 12,
children: selectedUsers
.map((user) => Chip(
avatar: ProfilePictureWidget(userId: user.uid, radius: 28),
label: Text('${user.firstName} ${user.lastName}',
style: const TextStyle(fontSize: 16)),
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
deleteIcon: const Icon(Icons.close, size: 20),
onDeleted: () {
final newList = List<String>.from(widget.selectedUserIds)
..remove(user.uid);
widget.onChanged(newList);
},
backgroundColor: Colors.grey[200],
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
))
.toList(),
UserChipsList(
users: selectedUsers,
selectedUserIds: widget.selectedUserIds,
showRemove: true,
onRemove: (uid) {
final newList = List<String>.from(widget.selectedUserIds)
..remove(uid);
widget.onChanged(newList);
},
),
const SizedBox(height: 16),
ElevatedButton.icon(
@ -188,3 +177,43 @@ class _UserPickerDialogState extends State<_UserPickerDialog> {
);
}
}
class UserChipsList extends StatelessWidget {
final List<UserModel> users;
final List<String> selectedUserIds;
final ValueChanged<String>? onRemove;
final bool showRemove;
final double avatarRadius;
const UserChipsList({
super.key,
required this.users,
this.selectedUserIds = const [],
this.onRemove,
this.showRemove = false,
this.avatarRadius = 28,
});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 12,
runSpacing: 12,
children: users
.map((user) => Chip(
avatar: ProfilePictureWidget(
userId: user.uid, radius: avatarRadius),
label: Text('${user.firstName} ${user.lastName}',
style: const TextStyle(fontSize: 16)),
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
deleteIcon:
showRemove ? const Icon(Icons.close, size: 20) : null,
onDeleted: showRemove && onRemove != null
? () => onRemove!(user.uid)
: null,
backgroundColor: Colors.grey[200],
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
))
.toList(),
);
}
}