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:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_dropzone/flutter_dropzone.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 {
|
class EventAddPage extends StatefulWidget {
|
||||||
const EventAddPage({super.key});
|
const EventAddPage({super.key});
|
||||||
@ -530,13 +532,12 @@ class _EventAddPageState extends State<EventAddPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _isLoadingUsers
|
child: UserMultiSelectWidget(
|
||||||
? const CircularProgressIndicator()
|
|
||||||
: UserMultiSelect(
|
|
||||||
allUsers: _allUsers,
|
allUsers: _allUsers,
|
||||||
selectedUserIds: _selectedUserIds,
|
selectedUserIds: _selectedUserIds,
|
||||||
onChanged: (ids) =>
|
onChanged: (ids) =>
|
||||||
setState(() => _selectedUserIds = ids),
|
setState(() => _selectedUserIds = ids),
|
||||||
|
isLoading: _isLoadingUsers,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@ -545,328 +546,14 @@ class _EventAddPageState extends State<EventAddPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSectionTitle('Documents'),
|
_buildSectionTitle('Documents'),
|
||||||
if (kIsWeb)
|
DropzoneUploadWidget(
|
||||||
Container(
|
uploadedFiles: _uploadedFiles,
|
||||||
constraints: const BoxConstraints(
|
onFilesChanged: (files) =>
|
||||||
minHeight: 180, minWidth: 200),
|
setState(() => _uploadedFiles = files),
|
||||||
decoration: BoxDecoration(
|
isLoading: _isLoading,
|
||||||
border: Border.all(
|
error: _error,
|
||||||
color: _isDropzoneHighlighted
|
success: _success,
|
||||||
? 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(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
SelectableText(
|
||||||
event.name,
|
event.name,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
color: AppColors.noir,
|
color: AppColors.noir,
|
||||||
@ -127,7 +127,7 @@ class EventDetails extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
SelectableText(
|
||||||
event.description,
|
event.description,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
@ -140,13 +140,13 @@ class EventDetails extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
SelectableText(
|
||||||
event.address,
|
event.address,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
if (event.latitude != 0.0 || event.longitude != 0.0) ...[
|
if (event.latitude != 0.0 || event.longitude != 0.0) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
SelectableText(
|
||||||
'${event.latitude}° N, ${event.longitude}° E',
|
'${event.latitude}° N, ${event.longitude}° E',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
@ -188,7 +188,12 @@ class EventDetails extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(icon, color: Colors.blueGrey),
|
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(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.download),
|
icon: const Icon(Icons.download),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
class ProfilePictureWidget extends StatelessWidget {
|
class ProfilePictureWidget extends StatefulWidget {
|
||||||
final String? userId; // Modifié pour être nullable
|
final String? userId;
|
||||||
final double radius;
|
final double radius;
|
||||||
final String? defaultImageUrl;
|
final String? defaultImageUrl;
|
||||||
|
|
||||||
@ -14,27 +14,57 @@ class ProfilePictureWidget extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<ProfilePictureWidget> createState() => _ProfilePictureWidgetState();
|
||||||
// Vérifier si userId est null ou vide
|
}
|
||||||
if (userId == null || userId!.isEmpty) {
|
|
||||||
return _buildDefaultAvatar(radius, defaultImageUrl);
|
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
||||||
|
late Future<DocumentSnapshot?> _userFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_userFuture = _getUserFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
return FutureBuilder<DocumentSnapshot>(
|
@override
|
||||||
future: FirebaseFirestore.instance.collection('users').doc(userId).get(),
|
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) {
|
||||||
|
if (widget.userId == null || widget.userId!.isEmpty) {
|
||||||
|
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FutureBuilder<DocumentSnapshot?>(
|
||||||
|
future: _userFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return _buildLoadingAvatar(radius);
|
return _buildLoadingAvatar(widget.radius);
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
print("Error loading profile: ${snapshot.error}");
|
print("Error loading profile: ${snapshot.error}");
|
||||||
return _buildDefaultAvatar(radius, defaultImageUrl);
|
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||||
} else if (snapshot.data != null && snapshot.data!.exists) {
|
} else if (snapshot.data != null && snapshot.data!.exists) {
|
||||||
final userData = snapshot.data!.data() as Map<String, dynamic>?;
|
final userData = snapshot.data!.data() as Map<String, dynamic>?;
|
||||||
final profilePhotoUrl = userData?['profilePhotoUrl'] as String?;
|
final profilePhotoUrl = userData?['profilePhotoUrl'] as String?;
|
||||||
|
|
||||||
if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) {
|
if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: radius,
|
radius: widget.radius,
|
||||||
backgroundImage: NetworkImage(profilePhotoUrl),
|
backgroundImage: NetworkImage(profilePhotoUrl),
|
||||||
onBackgroundImageError: (e, stack) {
|
onBackgroundImageError: (e, stack) {
|
||||||
print("Error loading profile image: $e");
|
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/models/user_model.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
class UserCard extends StatelessWidget {
|
class UserCard extends StatefulWidget {
|
||||||
final UserModel user;
|
final UserModel user;
|
||||||
final VoidCallback onEdit;
|
final VoidCallback onEdit;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
@ -16,6 +16,66 @@ class UserCard extends StatelessWidget {
|
|||||||
required this.onDelete,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final width = MediaQuery.of(context).size.width;
|
final width = MediaQuery.of(context).size.width;
|
||||||
@ -27,7 +87,7 @@ class UserCard extends StatelessWidget {
|
|||||||
elevation: 3,
|
elevation: 3,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: isMobile ? double.infinity : _desktopMaxWidth,
|
maxWidth: isMobile ? double.infinity : UserCard._desktopMaxWidth,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child:
|
child:
|
||||||
@ -47,13 +107,13 @@ class UserCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${user.firstName} ${user.lastName}",
|
"${widget.user.firstName} ${widget.user.lastName}",
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
user.email,
|
widget.user.email,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -65,7 +125,7 @@ class UserCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit, size: 20),
|
icon: const Icon(Icons.edit, size: 20),
|
||||||
onPressed: onEdit,
|
onPressed: widget.onEdit,
|
||||||
color: AppColors.rouge,
|
color: AppColors.rouge,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
@ -75,7 +135,7 @@ class UserCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, size: 20),
|
icon: const Icon(Icons.delete, size: 20),
|
||||||
onPressed: onDelete,
|
onPressed: widget.onDelete,
|
||||||
color: AppColors.gris,
|
color: AppColors.gris,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
@ -106,22 +166,22 @@ class UserCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${user.firstName} ${user.lastName}",
|
"${widget.user.firstName} ${widget.user.lastName}",
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
user.email,
|
widget.user.email,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (user.role.isNotEmpty) ...[
|
if (widget.user.role.isNotEmpty) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
user.role,
|
widget.user.role,
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
color: AppColors.gris,
|
color: AppColors.gris,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
@ -143,7 +203,7 @@ class UserCard extends StatelessWidget {
|
|||||||
_buildButton(
|
_buildButton(
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
label: "Modifier",
|
label: "Modifier",
|
||||||
onPressed: onEdit,
|
onPressed: widget.onEdit,
|
||||||
color: AppColors.rouge,
|
color: AppColors.rouge,
|
||||||
isNarrow: true,
|
isNarrow: true,
|
||||||
),
|
),
|
||||||
@ -151,7 +211,7 @@ class UserCard extends StatelessWidget {
|
|||||||
_buildButton(
|
_buildButton(
|
||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
label: "Supprimer",
|
label: "Supprimer",
|
||||||
onPressed: onDelete,
|
onPressed: widget.onDelete,
|
||||||
color: AppColors.gris,
|
color: AppColors.gris,
|
||||||
isNarrow: true,
|
isNarrow: true,
|
||||||
),
|
),
|
||||||
@ -163,7 +223,7 @@ class UserCard extends StatelessWidget {
|
|||||||
_buildButton(
|
_buildButton(
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
label: "Modifier",
|
label: "Modifier",
|
||||||
onPressed: onEdit,
|
onPressed: widget.onEdit,
|
||||||
color: AppColors.rouge,
|
color: AppColors.rouge,
|
||||||
isNarrow: false,
|
isNarrow: false,
|
||||||
),
|
),
|
||||||
@ -171,7 +231,7 @@ class UserCard extends StatelessWidget {
|
|||||||
_buildButton(
|
_buildButton(
|
||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
label: "Supprimer",
|
label: "Supprimer",
|
||||||
onPressed: onDelete,
|
onPressed: widget.onDelete,
|
||||||
color: AppColors.gris,
|
color: AppColors.gris,
|
||||||
isNarrow: false,
|
isNarrow: false,
|
||||||
),
|
),
|
||||||
@ -218,13 +278,22 @@ class UserCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _profileAvatar(double size) {
|
Widget _profileAvatar(double size) {
|
||||||
|
if (_isLoadingImage && widget.user.profilePhotoUrl.isNotEmpty) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: size / 2,
|
radius: size / 2,
|
||||||
backgroundImage: user.profilePhotoUrl.isNotEmpty
|
backgroundColor: Colors.grey[300],
|
||||||
? NetworkImage(user.profilePhotoUrl)
|
child: SizedBox(
|
||||||
: null,
|
width: size * 0.5,
|
||||||
|
height: size * 0.5,
|
||||||
|
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: size / 2,
|
||||||
|
backgroundImage: _profileImage,
|
||||||
backgroundColor: Colors.grey[200],
|
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)
|
? Icon(Icons.person, size: size * 0.6, color: AppColors.noir)
|
||||||
: null,
|
: 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
|
table_calendar: ^3.0.9
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
google_maps_flutter: ^2.5.0
|
google_maps_flutter: ^2.5.0
|
||||||
permission_handler: ^11.1.0
|
permission_handler: ^12.0.0+1
|
||||||
geolocator: ^10.1.0
|
geolocator: ^14.0.1
|
||||||
flutter_map: ^6.1.0
|
flutter_map: ^8.1.1
|
||||||
latlong2: ^0.9.0
|
latlong2: ^0.9.0
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.14.3
|
||||||
flutter_native_splash: ^2.3.9
|
flutter_native_splash: ^2.3.9
|
||||||
url_launcher: ^6.2.2
|
url_launcher: ^6.2.2
|
||||||
share_plus: ^7.2.1
|
share_plus: ^11.0.0
|
||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
pdf: ^3.10.7
|
pdf: ^3.10.7
|
||||||
printing: ^5.11.1
|
printing: ^5.11.1
|
||||||
flutter_local_notifications: ^16.3.0
|
flutter_local_notifications: ^19.2.1
|
||||||
timezone: ^0.9.2
|
timezone: ^0.10.1
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
http: ^1.1.2
|
http: ^1.1.2
|
||||||
flutter_dotenv: ^5.1.0
|
flutter_dotenv: ^5.1.0
|
||||||
@ -43,15 +43,15 @@ dependencies:
|
|||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
flutter_slidable: ^3.0.1
|
flutter_slidable: ^4.0.0
|
||||||
flutter_datetime_picker: ^1.5.1
|
flutter_datetime_picker: ^1.5.1
|
||||||
flutter_colorpicker: ^1.0.3
|
flutter_colorpicker: ^1.0.3
|
||||||
flutter_rating_bar: ^4.0.1
|
flutter_rating_bar: ^4.0.1
|
||||||
flutter_chat_ui: ^1.6.10
|
flutter_chat_ui: ^2.3.1
|
||||||
flutter_chat_types: ^3.6.2
|
flutter_chat_types: ^3.6.2
|
||||||
uuid: ^4.2.2
|
uuid: ^4.2.2
|
||||||
file_picker: ^6.1.1
|
file_picker: ^10.1.9
|
||||||
flutter_dropzone: ^3.0.6
|
flutter_dropzone: ^4.2.1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
@ -31,6 +31,17 @@
|
|||||||
|
|
||||||
<title>em2rp</title>
|
<title>em2rp</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
Reference in New Issue
Block a user