Dropzone OK et refactor page event_add
This commit is contained in:
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user