Ajout du choix des utilisateurs sur un événement. Ajout de fichiers dans un événement. (dropzone cassée)

This commit is contained in:
2025-05-26 22:10:40 +02:00
parent 82d77e2b8d
commit 49dffff1bf
1100 changed files with 157519 additions and 113 deletions

View File

@ -1,11 +1,22 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:latlong2/latlong.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:intl/intl.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart';
import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/views/widgets/image/profile_picture.dart';
import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:path/path.dart' as p;
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:firebase_auth/firebase_auth.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dropzone/flutter_dropzone.dart';
class EventAddPage extends StatefulWidget {
const EventAddPage({super.key});
@ -21,8 +32,7 @@ class _EventAddPageState extends State<EventAddPage> {
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;
@ -31,11 +41,18 @@ class _EventAddPageState extends State<EventAddPage> {
String? _selectedEventType;
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
int _descriptionMaxLines = 3;
List<String> _selectedUserIds = [];
List<UserModel> _allUsers = [];
bool _isLoadingUsers = true;
List<Map<String, String>> _uploadedFiles = [];
DropzoneViewController? _dropzoneController;
bool _isDropzoneHighlighted = false;
@override
void initState() {
super.initState();
_descriptionController.addListener(_handleDescriptionChange);
_fetchUsers();
}
void _handleDescriptionChange() {
@ -45,6 +62,16 @@ class _EventAddPageState extends State<EventAddPage> {
});
}
Future<void> _fetchUsers() async {
final snapshot = await FirebaseFirestore.instance.collection('users').get();
setState(() {
_allUsers = snapshot.docs
.map((doc) => UserModel.fromMap(doc.data(), doc.id))
.toList();
_isLoadingUsers = false;
});
}
@override
void dispose() {
_nameController.dispose();
@ -52,16 +79,93 @@ class _EventAddPageState extends State<EventAddPage> {
_priceController.dispose();
_installationController.dispose();
_disassemblyController.dispose();
_latitudeController.dispose();
_longitudeController.dispose();
_addressController.dispose();
super.dispose();
}
Future<void> _pickAndUploadFiles() async {
final result = await FilePicker.platform
.pickFiles(allowMultiple: true, withData: true);
if (result != null && result.files.isNotEmpty) {
setState(() => _isLoading = true);
try {
List<Map<String, String>> files = [];
for (final file in result.files) {
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes != null) {
final ref = FirebaseStorage.instance.ref().child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
final uploadTask = await ref.putData(fileBytes);
final url = await uploadTask.ref.getDownloadURL();
files.add({'name': fileName, 'url': url});
} else {
setState(() {
_error = "Impossible de lire le fichier ${file.name}";
});
}
}
setState(() {
_uploadedFiles.addAll(files);
});
} catch (e) {
setState(() {
_error = 'Erreur lors de l\'upload : $e';
});
} finally {
setState(() => _isLoading = false);
}
}
}
Future<String?> moveEventFileHttp({
required String sourcePath,
required String destinationPath,
}) async {
final url = Uri.parse(
'https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2');
final user = FirebaseAuth.instance.currentUser;
final idToken = await user?.getIdToken();
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
if (idToken != null) 'Authorization': 'Bearer $idToken',
},
body: jsonEncode({
'data': {
'sourcePath': sourcePath,
'destinationPath': destinationPath,
}
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['url'] != null) {
return data['url'] as String;
} else if (data['result'] != null && data['result']['url'] != null) {
return data['result']['url'] as String;
}
return null;
} else {
print('Erreur Cloud Function: \\n${response.body}');
return null;
}
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate() ||
_startDateTime == null ||
_endDateTime == null ||
_selectedEventType == null) return;
_selectedEventType == null ||
_addressController.text.isEmpty) return;
if (_endDateTime!.isBefore(_startDateTime!) ||
_endDateTime!.isAtSameMomentAs(_startDateTime!)) {
setState(() {
_error = "La date de fin doit être postérieure à la date de début.";
});
return;
}
setState(() {
_isLoading = true;
_error = null;
@ -79,14 +183,54 @@ class _EventAddPageState extends State<EventAddPage> {
installationTime: int.tryParse(_installationController.text) ?? 0,
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
eventTypeId: _selectedEventType!,
customerId: '', // à adapter si tu veux gérer les clients
address: LatLng(
double.tryParse(_latitudeController.text) ?? 0.0,
double.tryParse(_longitudeController.text) ?? 0.0,
),
workforce: [],
customerId: '',
address: _addressController.text.trim(),
workforce: _selectedUserIds
.map((id) => FirebaseFirestore.instance.collection('users').doc(id))
.toList(),
latitude: 0.0,
longitude: 0.0,
documents: _uploadedFiles,
);
await eventProvider.addEvent(newEvent);
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 !";
});
@ -203,6 +347,13 @@ class _EventAddPageState extends State<EventAddPage> {
time.hour,
time.minute,
);
if (_endDateTime != null &&
(_endDateTime!
.isBefore(_startDateTime!) ||
_endDateTime!.isAtSameMomentAs(
_startDateTime!))) {
_endDateTime = null;
}
});
}
}
@ -232,31 +383,34 @@ class _EventAddPageState extends State<EventAddPage> {
const SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: _startDateTime ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2099),
);
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,
onTap: _startDateTime == null
? null
: () async {
final picked = await showDatePicker(
context: context,
initialDate: _startDateTime!
.add(const Duration(hours: 1)),
firstDate: _startDateTime!,
lastDate: DateTime(2099),
);
});
}
}
},
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: AbsorbPointer(
child: TextFormField(
readOnly: true,
@ -274,7 +428,14 @@ class _EventAddPageState extends State<EventAddPage> {
),
validator: (v) => _endDateTime == null
? 'Champ requis'
: null,
: (_startDateTime != null &&
_endDateTime != null &&
(_endDateTime!.isBefore(
_startDateTime!) ||
_endDateTime!.isAtSameMomentAs(
_startDateTime!)))
? 'La date de fin doit être après la date de début'
: null,
),
),
),
@ -288,8 +449,28 @@ class _EventAddPageState extends State<EventAddPage> {
labelText: 'Prix (€)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.euro),
hintText: '1050.50',
),
keyboardType: TextInputType.number,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d*\.?\d{0,2}')),
],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le prix est requis';
}
final price =
double.tryParse(value.replaceAll(',', '.'));
if (price == null) {
return 'Veuillez entrer un nombre valide';
}
if (price < 0) {
return 'Le prix ne peut pas être négatif';
}
return null;
},
),
_buildSectionTitle('Détails'),
AnimatedContainer(
@ -319,7 +500,7 @@ class _EventAddPageState extends State<EventAddPage> {
label: 'Installation (h)',
controller: _installationController,
min: 0,
max: 24,
max: 99,
),
),
const SizedBox(width: 16),
@ -328,51 +509,369 @@ class _EventAddPageState extends State<EventAddPage> {
label: 'Démontage (h)',
controller: _disassemblyController,
min: 0,
max: 24,
max: 99,
),
),
],
),
_buildSectionTitle('Localisation'),
_buildSectionTitle('Adresse'),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(
labelText: 'Adresse',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
validator: (v) =>
v == null || v.isEmpty ? 'Champ requis' : null,
),
_buildSectionTitle('Personnel'),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextFormField(
controller: _latitudeController,
decoration: const InputDecoration(
labelText: 'Latitude',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
keyboardType: TextInputType.number,
),
child: _isLoadingUsers
? const CircularProgressIndicator()
: UserMultiSelect(
allUsers: _allUsers,
selectedUserIds: _selectedUserIds,
onChanged: (ids) =>
setState(() => _selectedUserIds = ids),
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _longitudeController,
decoration: const InputDecoration(
labelText: 'Longitude',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
keyboardType: TextInputType.number,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Documents'),
if (kIsWeb)
Container(
constraints: const BoxConstraints(
minHeight: 180, minWidth: 200),
decoration: BoxDecoration(
border: Border.all(
color: _isDropzoneHighlighted
? Colors.blue
: Colors.grey,
width: 2,
),
borderRadius: BorderRadius.circular(8),
color: _isDropzoneHighlighted
? Colors.blue.withOpacity(0.1)
: null,
),
child: Stack(
children: [
DropzoneView(
onCreated: (controller) =>
_dropzoneController = controller,
onDrop: (ev) async {
setState(() => _isLoading = true);
try {
final name =
await _dropzoneController!
.getFilename(ev);
final bytes =
await _dropzoneController!
.getFileData(ev);
if (bytes != null) {
final ref = FirebaseStorage
.instance
.ref()
.child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
final uploadTask =
await ref.putData(bytes);
final url = await uploadTask.ref
.getDownloadURL();
setState(() {
_uploadedFiles.add(
{'name': name, 'url': url});
});
}
} catch (e) {
setState(() {
_error =
'Erreur lors de l\'upload : $e';
});
} finally {
setState(() {
_isLoading = false;
_isDropzoneHighlighted = false;
});
}
},
onHover: () => setState(() =>
_isDropzoneHighlighted = true),
onLeave: () => setState(() =>
_isDropzoneHighlighted = false),
onDropMultiple: (evs) async {
setState(() => _isLoading = true);
try {
if (evs != null) {
for (final ev in evs) {
final name =
await _dropzoneController!
.getFilename(ev);
final bytes =
await _dropzoneController!
.getFileData(ev);
if (bytes != null) {
final ref = FirebaseStorage
.instance
.ref()
.child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
final uploadTask =
await ref.putData(bytes);
final url = await uploadTask
.ref
.getDownloadURL();
setState(() {
_uploadedFiles.add({
'name': name,
'url': url
});
});
}
}
}
} catch (e) {
setState(() {
_error =
'Erreur lors de l\'upload : $e';
});
} finally {
setState(() {
_isLoading = false;
_isDropzoneHighlighted = false;
});
}
},
),
Positioned.fill(
child: IgnorePointer(
child: _uploadedFiles.isEmpty
? Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
const Icon(
Icons.cloud_upload,
size: 48,
color: Colors.grey),
const SizedBox(height: 12),
const Text(
'Glissez-déposez des fichiers ici ou cliquez sur "Ajouter"',
textAlign:
TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 16),
),
const SizedBox(height: 16),
],
)
: null,
),
),
if (_uploadedFiles.isNotEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
child: _buildFileListAndButton(),
),
if (_uploadedFiles.isEmpty)
Positioned(
bottom: 16,
left: 0,
right: 0,
child: Center(
child: SizedBox(
width: 160,
child: ElevatedButton.icon(
icon: const Icon(
Icons.attach_file,
size: 18),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets
.symmetric(
horizontal: 8,
vertical: 8),
minimumSize:
const Size(80, 36),
textStyle: const TextStyle(
fontSize: 14),
),
onPressed: _isLoading
? null
: _pickAndUploadFiles,
),
),
),
),
if (_error != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Padding(
padding:
const EdgeInsets.only(top: 8.0),
child: Text(_error!,
style: const TextStyle(
color: Colors.red)),
),
),
if (_success != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.only(
top: 32.0),
child: Text(_success!,
style: const TextStyle(
color: Colors.green)),
),
),
],
),
)
else if ([
TargetPlatform.windows,
TargetPlatform.macOS,
TargetPlatform.linux
].contains(defaultTargetPlatform))
DragTarget<PlatformFile>(
onWillAccept: (data) => true,
onAccept: (file) async {
setState(() => _isLoading = true);
try {
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes != null) {
final ref = FirebaseStorage.instance
.ref()
.child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
final uploadTask =
await ref.putData(fileBytes);
final url = await uploadTask.ref
.getDownloadURL();
setState(() {
_uploadedFiles.add(
{'name': fileName, 'url': url});
});
}
} catch (e) {
setState(() {
_error =
'Erreur lors de l\'upload : $e';
});
} finally {
setState(() => _isLoading = false);
}
},
builder:
(context, candidateData, rejectedData) {
final hasFiles = _uploadedFiles.isNotEmpty;
return Container(
constraints: const BoxConstraints(
minHeight: 180, minWidth: 200),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: candidateData.isNotEmpty
? Colors.blue
: Colors.grey,
width: 2,
),
borderRadius: BorderRadius.circular(8),
color: candidateData.isNotEmpty
? Colors.blue.withOpacity(0.1)
: null,
),
child: hasFiles
? _buildFileListAndButton()
: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
const Icon(Icons.cloud_upload,
size: 48,
color: Colors.grey),
const SizedBox(height: 12),
const Text(
'Glissez-déposez des fichiers ici ou cliquez sur "Ajouter"',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 16),
),
const SizedBox(height: 16),
SizedBox(
width: 160,
child: ElevatedButton.icon(
icon: const Icon(
Icons.attach_file,
size: 18),
label:
const Text('Ajouter'),
style: ElevatedButton
.styleFrom(
padding: const EdgeInsets
.symmetric(
horizontal: 8,
vertical: 8),
minimumSize:
const Size(80, 36),
textStyle:
const TextStyle(
fontSize: 14),
),
onPressed: _isLoading
? null
: _pickAndUploadFiles,
),
),
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)),
),
],
),
);
},
)
else
_buildFileListAndButton(),
],
),
),
],
),
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)),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
@ -407,4 +906,219 @@ class _EventAddPageState extends State<EventAddPage> {
),
);
}
Widget _buildFileListAndButton() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._uploadedFiles.map((file) {
final fileName = file['name']!;
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"].contains(ext)) {
icon = Icons.description;
} else {
icon = Icons.attach_file;
}
return ListTile(
leading: Icon(icon, color: Colors.blueGrey),
title: Text(fileName, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
_uploadedFiles.remove(file);
});
},
),
contentPadding: EdgeInsets.zero,
dense: true,
);
}).toList(),
SizedBox(
width: 160,
child: ElevatedButton.icon(
icon: const Icon(Icons.attach_file, size: 18),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
minimumSize: const Size(80, 36),
textStyle: const TextStyle(fontSize: 14),
),
onPressed: _isLoading ? null : _pickAndUploadFiles,
),
),
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)),
),
],
);
}
}
class UserMultiSelect extends StatefulWidget {
final List<UserModel> allUsers;
final List<String> selectedUserIds;
final ValueChanged<List<String>> onChanged;
const UserMultiSelect({
super.key,
required this.allUsers,
required this.selectedUserIds,
required this.onChanged,
});
@override
State<UserMultiSelect> createState() => _UserMultiSelectState();
}
class _UserMultiSelectState extends State<UserMultiSelect> {
void _openUserPicker() async {
final result = await showDialog<List<String>>(
context: context,
builder: (context) => _UserPickerDialog(
allUsers: widget.allUsers,
initiallySelected: widget.selectedUserIds,
),
);
if (result != null) {
widget.onChanged(result);
}
}
@override
Widget build(BuildContext context) {
final selectedUsers = widget.allUsers
.where((u) => widget.selectedUserIds.contains(u.uid))
.toList();
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(),
),
const SizedBox(height: 16),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
onPressed: _openUserPicker,
),
],
);
}
}
class _UserPickerDialog extends StatefulWidget {
final List<UserModel> allUsers;
final List<String> initiallySelected;
const _UserPickerDialog(
{required this.allUsers, required this.initiallySelected});
@override
State<_UserPickerDialog> createState() => _UserPickerDialogState();
}
class _UserPickerDialogState extends State<_UserPickerDialog> {
String _search = '';
late List<String> _selected;
@override
void initState() {
super.initState();
_selected = List<String>.from(widget.initiallySelected);
}
@override
Widget build(BuildContext context) {
final filteredUsers = widget.allUsers.where((u) {
final query = _search.toLowerCase();
return ('${u.firstName} ${u.lastName}').toLowerCase().contains(query);
}).toList();
return AlertDialog(
title: const Text('Ajouter du personnel'),
content: SizedBox(
width: 400,
height: 400,
child: Column(
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Rechercher',
prefixIcon: Icon(Icons.search),
),
onChanged: (v) => setState(() => _search = v),
),
const SizedBox(height: 12),
Expanded(
child: filteredUsers.isEmpty
? const Center(child: Text('Aucun utilisateur trouvé'))
: ListView.builder(
itemCount: filteredUsers.length,
itemBuilder: (context, i) {
final user = filteredUsers[i];
final isChecked = _selected.contains(user.uid);
return CheckboxListTile(
value: isChecked,
onChanged: (checked) {
setState(() {
if (checked == true) {
_selected.add(user.uid);
} else {
_selected.remove(user.uid);
}
});
},
title: Text('${user.firstName} ${user.lastName}'),
subtitle: Text(user.email),
secondary: ProfilePictureWidget(
userId: user.uid, radius: 20),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, _selected),
child: const Text('Ajouter'),
),
],
);
}
}