Dropzone OK et refactor page event_add

This commit is contained in:
2025-05-27 20:08:39 +02:00
parent 49dffff1bf
commit 9489183b68
8 changed files with 627 additions and 590 deletions

View File

@ -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'),
),
],
);
}
}