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

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

View File

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