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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user