Dropzone OK et refactor page event_add
This commit is contained in:
@ -17,6 +17,8 @@ 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';
|
||||
import 'package:em2rp/views/widgets/inputs/dropzone_upload_widget.dart';
|
||||
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
||||
|
||||
class EventAddPage extends StatefulWidget {
|
||||
const EventAddPage({super.key});
|
||||
@ -530,14 +532,13 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _isLoadingUsers
|
||||
? const CircularProgressIndicator()
|
||||
: UserMultiSelect(
|
||||
allUsers: _allUsers,
|
||||
selectedUserIds: _selectedUserIds,
|
||||
onChanged: (ids) =>
|
||||
setState(() => _selectedUserIds = ids),
|
||||
),
|
||||
child: UserMultiSelectWidget(
|
||||
allUsers: _allUsers,
|
||||
selectedUserIds: _selectedUserIds,
|
||||
onChanged: (ids) =>
|
||||
setState(() => _selectedUserIds = ids),
|
||||
isLoading: _isLoadingUsers,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@ -545,328 +546,14 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
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(),
|
||||
DropzoneUploadWidget(
|
||||
uploadedFiles: _uploadedFiles,
|
||||
onFilesChanged: (files) =>
|
||||
setState(() => _uploadedFiles = files),
|
||||
isLoading: _isLoading,
|
||||
error: _error,
|
||||
success: _success,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -906,219 +593,4 @@ 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class EventDetails extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
SelectableText(
|
||||
event.name,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: AppColors.noir,
|
||||
@ -127,7 +127,7 @@ class EventDetails extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
SelectableText(
|
||||
event.description,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
@ -140,13 +140,13 @@ class EventDetails extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
SelectableText(
|
||||
event.address,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (event.latitude != 0.0 || event.longitude != 0.0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
SelectableText(
|
||||
'${event.latitude}° N, ${event.longitude}° E',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
@ -188,7 +188,12 @@ class EventDetails extends StatelessWidget {
|
||||
}
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Colors.blueGrey),
|
||||
title: Text(fileName, overflow: TextOverflow.ellipsis),
|
||||
title: SelectableText(
|
||||
fileName,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () async {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class ProfilePictureWidget extends StatelessWidget {
|
||||
final String? userId; // Modifié pour être nullable
|
||||
class ProfilePictureWidget extends StatefulWidget {
|
||||
final String? userId;
|
||||
final double radius;
|
||||
final String? defaultImageUrl;
|
||||
|
||||
@ -13,28 +13,58 @@ class ProfilePictureWidget extends StatelessWidget {
|
||||
this.defaultImageUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProfilePictureWidget> createState() => _ProfilePictureWidgetState();
|
||||
}
|
||||
|
||||
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
||||
late Future<DocumentSnapshot?> _userFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_userFuture = _getUserFuture();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ProfilePictureWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userId != widget.userId) {
|
||||
_userFuture = _getUserFuture();
|
||||
}
|
||||
}
|
||||
|
||||
Future<DocumentSnapshot?> _getUserFuture() {
|
||||
if (widget.userId == null || widget.userId!.isEmpty) {
|
||||
return Future.value(null);
|
||||
}
|
||||
return FirebaseFirestore.instance
|
||||
.collection('users')
|
||||
.doc(widget.userId)
|
||||
.get();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Vérifier si userId est null ou vide
|
||||
if (userId == null || userId!.isEmpty) {
|
||||
return _buildDefaultAvatar(radius, defaultImageUrl);
|
||||
if (widget.userId == null || widget.userId!.isEmpty) {
|
||||
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||
}
|
||||
|
||||
return FutureBuilder<DocumentSnapshot>(
|
||||
future: FirebaseFirestore.instance.collection('users').doc(userId).get(),
|
||||
return FutureBuilder<DocumentSnapshot?>(
|
||||
future: _userFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return _buildLoadingAvatar(radius);
|
||||
return _buildLoadingAvatar(widget.radius);
|
||||
} else if (snapshot.hasError) {
|
||||
print("Error loading profile: ${snapshot.error}");
|
||||
return _buildDefaultAvatar(radius, defaultImageUrl);
|
||||
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||
} else if (snapshot.data != null && snapshot.data!.exists) {
|
||||
final userData = snapshot.data!.data() as Map<String, dynamic>?;
|
||||
final profilePhotoUrl = userData?['profilePhotoUrl'] as String?;
|
||||
|
||||
if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
radius: widget.radius,
|
||||
backgroundImage: NetworkImage(profilePhotoUrl),
|
||||
onBackgroundImageError: (e, stack) {
|
||||
print("Error loading profile image: $e");
|
||||
@ -42,7 +72,7 @@ class ProfilePictureWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
return _buildDefaultAvatar(radius, defaultImageUrl);
|
||||
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
260
em2rp/lib/views/widgets/inputs/dropzone_upload_widget.dart
Normal file
260
em2rp/lib/views/widgets/inputs/dropzone_upload_widget.dart
Normal file
@ -0,0 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:flutter_dropzone/flutter_dropzone.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class DropzoneUploadWidget extends StatefulWidget {
|
||||
final List<Map<String, String>> uploadedFiles;
|
||||
final ValueChanged<List<Map<String, String>>> onFilesChanged;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final String? success;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const DropzoneUploadWidget({
|
||||
super.key,
|
||||
required this.uploadedFiles,
|
||||
required this.onFilesChanged,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.success,
|
||||
this.width = 400,
|
||||
this.height = 200,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DropzoneUploadWidget> createState() => _DropzoneUploadWidgetState();
|
||||
}
|
||||
|
||||
class _DropzoneUploadWidgetState extends State<DropzoneUploadWidget> {
|
||||
DropzoneViewController? _dropzoneController;
|
||||
bool _isDropzoneHighlighted = false;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String? _success;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isLoading = widget.isLoading;
|
||||
_error = widget.error;
|
||||
_success = widget.success;
|
||||
}
|
||||
|
||||
Future<void> _handleFiles(List<dynamic> files) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
List<Map<String, String>> newFiles = List.from(widget.uploadedFiles);
|
||||
for (final file in files) {
|
||||
final name = await _dropzoneController!.getFilename(file);
|
||||
final bytes = await _dropzoneController!.getFileData(file);
|
||||
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();
|
||||
if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) {
|
||||
newFiles.add({'name': name, 'url': url});
|
||||
}
|
||||
}
|
||||
}
|
||||
widget.onFilesChanged(newFiles);
|
||||
setState(() {
|
||||
_success = "Fichier(s) ajouté(s) !";
|
||||
_error = null;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Erreur lors de l\'upload : $e';
|
||||
_success = null;
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isDropzoneHighlighted = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Container(
|
||||
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,
|
||||
onDropFiles: (files) async {
|
||||
if (files == null) return;
|
||||
await _handleFiles(files);
|
||||
},
|
||||
onHover: () => setState(() => _isDropzoneHighlighted = true),
|
||||
onLeave: () => setState(() => _isDropzoneHighlighted = false),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: widget.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 (widget.uploadedFiles.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildFileListAndButton(),
|
||||
),
|
||||
if (widget.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
|
||||
: () async {
|
||||
final files =
|
||||
await _dropzoneController?.pickFiles();
|
||||
if (files != null) {
|
||||
await _handleFiles(files);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isLoading)
|
||||
const Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: Color.fromARGB(80, 255, 255, 255),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: 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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileListAndButton() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...widget.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: _isLoading
|
||||
? null
|
||||
: () {
|
||||
final newFiles =
|
||||
List<Map<String, String>>.from(widget.uploadedFiles)
|
||||
..remove(file);
|
||||
widget.onFilesChanged(newFiles);
|
||||
},
|
||||
),
|
||||
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
|
||||
: () async {
|
||||
final files = await _dropzoneController?.pickFiles();
|
||||
if (files != null) {
|
||||
await _handleFiles(files);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
class UserCard extends StatelessWidget {
|
||||
class UserCard extends StatefulWidget {
|
||||
final UserModel user;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
@ -16,6 +16,66 @@ class UserCard extends StatelessWidget {
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UserCard> createState() => _UserCardState();
|
||||
}
|
||||
|
||||
class _UserCardState extends State<UserCard> {
|
||||
ImageProvider? _profileImage;
|
||||
String? _lastUrl;
|
||||
bool _isLoadingImage = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(UserCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.user.profilePhotoUrl != widget.user.profilePhotoUrl) {
|
||||
_loadProfileImage();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfileImage();
|
||||
}
|
||||
|
||||
void _loadProfileImage() {
|
||||
final url = widget.user.profilePhotoUrl;
|
||||
if (url.isNotEmpty) {
|
||||
setState(() {
|
||||
_isLoadingImage = true;
|
||||
_lastUrl = url;
|
||||
});
|
||||
final image = NetworkImage(url);
|
||||
image.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener(
|
||||
(info, _) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_profileImage = image;
|
||||
_isLoadingImage = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error, stack) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_profileImage = null;
|
||||
_isLoadingImage = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_profileImage = null;
|
||||
_isLoadingImage = false;
|
||||
_lastUrl = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
@ -27,7 +87,7 @@ class UserCard extends StatelessWidget {
|
||||
elevation: 3,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isMobile ? double.infinity : _desktopMaxWidth,
|
||||
maxWidth: isMobile ? double.infinity : UserCard._desktopMaxWidth,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child:
|
||||
@ -47,13 +107,13 @@ class UserCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${user.firstName} ${user.lastName}",
|
||||
"${widget.user.firstName} ${widget.user.lastName}",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
user.email,
|
||||
widget.user.email,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -65,7 +125,7 @@ class UserCard extends StatelessWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
onPressed: onEdit,
|
||||
onPressed: widget.onEdit,
|
||||
color: AppColors.rouge,
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
@ -75,7 +135,7 @@ class UserCard extends StatelessWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20),
|
||||
onPressed: onDelete,
|
||||
onPressed: widget.onDelete,
|
||||
color: AppColors.gris,
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
@ -106,22 +166,22 @@ class UserCard extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"${user.firstName} ${user.lastName}",
|
||||
"${widget.user.firstName} ${widget.user.lastName}",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
user.email,
|
||||
widget.user.email,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (user.role.isNotEmpty) ...[
|
||||
if (widget.user.role.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
user.role,
|
||||
widget.user.role,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: AppColors.gris,
|
||||
fontSize: 11,
|
||||
@ -143,7 +203,7 @@ class UserCard extends StatelessWidget {
|
||||
_buildButton(
|
||||
icon: Icons.edit,
|
||||
label: "Modifier",
|
||||
onPressed: onEdit,
|
||||
onPressed: widget.onEdit,
|
||||
color: AppColors.rouge,
|
||||
isNarrow: true,
|
||||
),
|
||||
@ -151,7 +211,7 @@ class UserCard extends StatelessWidget {
|
||||
_buildButton(
|
||||
icon: Icons.delete,
|
||||
label: "Supprimer",
|
||||
onPressed: onDelete,
|
||||
onPressed: widget.onDelete,
|
||||
color: AppColors.gris,
|
||||
isNarrow: true,
|
||||
),
|
||||
@ -163,7 +223,7 @@ class UserCard extends StatelessWidget {
|
||||
_buildButton(
|
||||
icon: Icons.edit,
|
||||
label: "Modifier",
|
||||
onPressed: onEdit,
|
||||
onPressed: widget.onEdit,
|
||||
color: AppColors.rouge,
|
||||
isNarrow: false,
|
||||
),
|
||||
@ -171,7 +231,7 @@ class UserCard extends StatelessWidget {
|
||||
_buildButton(
|
||||
icon: Icons.delete,
|
||||
label: "Supprimer",
|
||||
onPressed: onDelete,
|
||||
onPressed: widget.onDelete,
|
||||
color: AppColors.gris,
|
||||
isNarrow: false,
|
||||
),
|
||||
@ -218,13 +278,22 @@ class UserCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _profileAvatar(double size) {
|
||||
if (_isLoadingImage && widget.user.profilePhotoUrl.isNotEmpty) {
|
||||
return CircleAvatar(
|
||||
radius: size / 2,
|
||||
backgroundColor: Colors.grey[300],
|
||||
child: SizedBox(
|
||||
width: size * 0.5,
|
||||
height: size * 0.5,
|
||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
return CircleAvatar(
|
||||
radius: size / 2,
|
||||
backgroundImage: user.profilePhotoUrl.isNotEmpty
|
||||
? NetworkImage(user.profilePhotoUrl)
|
||||
: null,
|
||||
backgroundImage: _profileImage,
|
||||
backgroundColor: Colors.grey[200],
|
||||
child: user.profilePhotoUrl.isEmpty
|
||||
child: (widget.user.profilePhotoUrl.isEmpty || _profileImage == null)
|
||||
? Icon(Icons.person, size: size * 0.6, color: AppColors.noir)
|
||||
: null,
|
||||
);
|
||||
|
@ -0,0 +1,190 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||
|
||||
class UserMultiSelectWidget extends StatelessWidget {
|
||||
final List<UserModel> allUsers;
|
||||
final List<String> selectedUserIds;
|
||||
final ValueChanged<List<String>> onChanged;
|
||||
final bool isLoading;
|
||||
|
||||
const UserMultiSelectWidget({
|
||||
super.key,
|
||||
required this.allUsers,
|
||||
required this.selectedUserIds,
|
||||
required this.onChanged,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading) {
|
||||
return const Center(
|
||||
child:
|
||||
SizedBox(width: 32, height: 32, child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return _UserMultiSelect(
|
||||
allUsers: allUsers,
|
||||
selectedUserIds: selectedUserIds,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -22,19 +22,19 @@ dependencies:
|
||||
table_calendar: ^3.0.9
|
||||
intl: ^0.19.0
|
||||
google_maps_flutter: ^2.5.0
|
||||
permission_handler: ^11.1.0
|
||||
geolocator: ^10.1.0
|
||||
flutter_map: ^6.1.0
|
||||
permission_handler: ^12.0.0+1
|
||||
geolocator: ^14.0.1
|
||||
flutter_map: ^8.1.1
|
||||
latlong2: ^0.9.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_launcher_icons: ^0.14.3
|
||||
flutter_native_splash: ^2.3.9
|
||||
url_launcher: ^6.2.2
|
||||
share_plus: ^7.2.1
|
||||
share_plus: ^11.0.0
|
||||
path_provider: ^2.1.2
|
||||
pdf: ^3.10.7
|
||||
printing: ^5.11.1
|
||||
flutter_local_notifications: ^16.3.0
|
||||
timezone: ^0.9.2
|
||||
flutter_local_notifications: ^19.2.1
|
||||
timezone: ^0.10.1
|
||||
flutter_secure_storage: ^9.0.0
|
||||
http: ^1.1.2
|
||||
flutter_dotenv: ^5.1.0
|
||||
@ -43,15 +43,15 @@ dependencies:
|
||||
cached_network_image: ^3.3.1
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
shimmer: ^3.0.0
|
||||
flutter_slidable: ^3.0.1
|
||||
flutter_slidable: ^4.0.0
|
||||
flutter_datetime_picker: ^1.5.1
|
||||
flutter_colorpicker: ^1.0.3
|
||||
flutter_rating_bar: ^4.0.1
|
||||
flutter_chat_ui: ^1.6.10
|
||||
flutter_chat_ui: ^2.3.1
|
||||
flutter_chat_types: ^3.6.2
|
||||
uuid: ^4.2.2
|
||||
file_picker: ^6.1.1
|
||||
flutter_dropzone: ^3.0.6
|
||||
file_picker: ^10.1.9
|
||||
flutter_dropzone: ^4.2.1
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
|
@ -31,6 +31,17 @@
|
||||
|
||||
<title>em2rp</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<style>
|
||||
html, body, #flt-glass-pane {
|
||||
user-select: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
}
|
||||
.dropzone-view, .dropzone-view * {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
|
Reference in New Issue
Block a user