Authentification stable - WIP
This commit is contained in:
@@ -1,627 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
import '../widgets/sophisticated_member_card.dart';
|
||||
import '../widgets/members_search_bar.dart';
|
||||
import '../widgets/members_filter_sheet.dart';
|
||||
|
||||
class MembersListPage extends StatefulWidget {
|
||||
const MembersListPage({super.key});
|
||||
|
||||
@override
|
||||
State<MembersListPage> createState() => _MembersListPageState();
|
||||
}
|
||||
|
||||
class _MembersListPageState extends State<MembersListPage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
String _searchQuery = '';
|
||||
String _selectedFilter = 'Tous';
|
||||
bool _isSearchActive = false;
|
||||
|
||||
final List<Map<String, dynamic>> _members = [
|
||||
{
|
||||
'id': '1',
|
||||
'firstName': 'Jean',
|
||||
'lastName': 'Dupont',
|
||||
'email': 'jean.dupont@email.com',
|
||||
'phone': '+33 6 12 34 56 78',
|
||||
'role': 'Président',
|
||||
'status': 'Actif',
|
||||
'joinDate': '2022-01-15',
|
||||
'lastActivity': '2024-08-15',
|
||||
'cotisationStatus': 'À jour',
|
||||
'avatar': null,
|
||||
'category': 'Bureau',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'firstName': 'Marie',
|
||||
'lastName': 'Martin',
|
||||
'email': 'marie.martin@email.com',
|
||||
'phone': '+33 6 98 76 54 32',
|
||||
'role': 'Secrétaire',
|
||||
'status': 'Actif',
|
||||
'joinDate': '2022-03-20',
|
||||
'lastActivity': '2024-08-14',
|
||||
'cotisationStatus': 'À jour',
|
||||
'avatar': null,
|
||||
'category': 'Bureau',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'firstName': 'Pierre',
|
||||
'lastName': 'Dubois',
|
||||
'email': 'pierre.dubois@email.com',
|
||||
'phone': '+33 6 55 44 33 22',
|
||||
'role': 'Trésorier',
|
||||
'status': 'Actif',
|
||||
'joinDate': '2022-02-10',
|
||||
'lastActivity': '2024-08-13',
|
||||
'cotisationStatus': 'En retard',
|
||||
'avatar': null,
|
||||
'category': 'Bureau',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'firstName': 'Sophie',
|
||||
'lastName': 'Leroy',
|
||||
'email': 'sophie.leroy@email.com',
|
||||
'phone': '+33 6 11 22 33 44',
|
||||
'role': 'Membre',
|
||||
'status': 'Actif',
|
||||
'joinDate': '2023-05-12',
|
||||
'lastActivity': '2024-08-12',
|
||||
'cotisationStatus': 'À jour',
|
||||
'avatar': null,
|
||||
'category': 'Membres',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'firstName': 'Thomas',
|
||||
'lastName': 'Roux',
|
||||
'email': 'thomas.roux@email.com',
|
||||
'phone': '+33 6 77 88 99 00',
|
||||
'role': 'Membre',
|
||||
'status': 'Inactif',
|
||||
'joinDate': '2021-09-08',
|
||||
'lastActivity': '2024-07-20',
|
||||
'cotisationStatus': 'En retard',
|
||||
'avatar': null,
|
||||
'category': 'Membres',
|
||||
},
|
||||
{
|
||||
'id': '6',
|
||||
'firstName': 'Emma',
|
||||
'lastName': 'Moreau',
|
||||
'email': 'emma.moreau@email.com',
|
||||
'phone': '+33 6 66 77 88 99',
|
||||
'role': 'Responsable événements',
|
||||
'status': 'Actif',
|
||||
'joinDate': '2023-01-25',
|
||||
'lastActivity': '2024-08-16',
|
||||
'cotisationStatus': 'À jour',
|
||||
'avatar': null,
|
||||
'category': 'Responsables',
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_searchController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> get _filteredMembers {
|
||||
return _members.where((member) {
|
||||
final matchesSearch = _searchQuery.isEmpty ||
|
||||
member['firstName'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
member['lastName'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
member['email'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
member['role'].toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
|
||||
final matchesFilter = _selectedFilter == 'Tous' ||
|
||||
(_selectedFilter == 'Actifs' && member['status'] == 'Actif') ||
|
||||
(_selectedFilter == 'Inactifs' && member['status'] == 'Inactif') ||
|
||||
(_selectedFilter == 'Bureau' && member['category'] == 'Bureau') ||
|
||||
(_selectedFilter == 'En retard' && member['cotisationStatus'] == 'En retard');
|
||||
|
||||
return matchesSearch && matchesFilter;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ResponsiveUtils.init(context);
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
body: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
_buildAppBar(innerBoxIsScrolled),
|
||||
_buildTabBar(),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildMembersList(),
|
||||
_buildMembersList(filter: 'Bureau'),
|
||||
_buildMembersList(filter: 'Responsables'),
|
||||
_buildMembersList(filter: 'Membres'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(bool innerBoxIsScrolled) {
|
||||
return SliverAppBar(
|
||||
expandedHeight: _isSearchActive ? 250 : 180,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: AnimatedOpacity(
|
||||
opacity: innerBoxIsScrolled ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const Text(
|
||||
'Membres',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppTheme.secondaryColor, AppTheme.secondaryColor.withOpacity(0.8)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Titre principal quand l'AppBar est étendu
|
||||
if (!innerBoxIsScrolled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 60),
|
||||
child: Text(
|
||||
'Membres',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu principal
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: ResponsiveUtils.paddingOnly(
|
||||
left: 4,
|
||||
top: 2,
|
||||
right: 4,
|
||||
bottom: 2,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isSearchActive) ...[
|
||||
Flexible(
|
||||
child: MembersSearchBar(
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
onClear: () {
|
||||
setState(() {
|
||||
_searchQuery = '';
|
||||
_searchController.clear();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.hp),
|
||||
],
|
||||
Flexible(
|
||||
child: _buildStatsRow(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isSearchActive ? Icons.search_off : Icons.search),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isSearchActive = !_isSearchActive;
|
||||
if (!_isSearchActive) {
|
||||
_searchController.clear();
|
||||
_searchQuery = '';
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: _showFilterSheet,
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: _handleMenuSelection,
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.download),
|
||||
SizedBox(width: 8),
|
||||
Text('Exporter'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'import',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.upload),
|
||||
SizedBox(width: 8),
|
||||
Text('Importer'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'stats',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.analytics),
|
||||
SizedBox(width: 8),
|
||||
Text('Statistiques'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return SliverPersistentHeader(
|
||||
delegate: _TabBarDelegate(
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppTheme.secondaryColor,
|
||||
unselectedLabelColor: AppTheme.textSecondary,
|
||||
indicatorColor: AppTheme.secondaryColor,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
|
||||
tabs: const [
|
||||
Tab(text: 'Tous'),
|
||||
Tab(text: 'Bureau'),
|
||||
Tab(text: 'Responsables'),
|
||||
Tab(text: 'Membres'),
|
||||
],
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsRow() {
|
||||
final activeCount = _members.where((m) => m['status'] == 'Actif').length;
|
||||
final latePayments = _members.where((m) => m['cotisationStatus'] == 'En retard').length;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_buildStatCard(
|
||||
title: 'Total',
|
||||
value: '${_members.length}',
|
||||
icon: Icons.people,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatCard(
|
||||
title: 'Actifs',
|
||||
value: '$activeCount',
|
||||
icon: Icons.check_circle,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatCard(
|
||||
title: 'En retard',
|
||||
value: '$latePayments',
|
||||
icon: Icons.warning,
|
||||
color: AppTheme.warningColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: ResponsiveUtils.iconSize(4),
|
||||
),
|
||||
SizedBox(width: 1.5.wp),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 3.5.fs,
|
||||
medium: 3.2.fs,
|
||||
large: 3.fs,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: ResponsiveUtils.adaptive(
|
||||
small: 2.8.fs,
|
||||
medium: 2.6.fs,
|
||||
large: 2.4.fs,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembersList({String? filter}) {
|
||||
List<Map<String, dynamic>> members = _filteredMembers;
|
||||
|
||||
if (filter != null) {
|
||||
members = members.where((member) => member['category'] == filter).toList();
|
||||
}
|
||||
|
||||
if (members.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshMembers,
|
||||
color: AppTheme.secondaryColor,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: members.length,
|
||||
itemBuilder: (context, index) {
|
||||
final member = members[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: SophisticatedMemberCard(
|
||||
member: member,
|
||||
onTap: () => _showMemberDetails(member),
|
||||
onEdit: () => _editMember(member),
|
||||
compact: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.people_outline,
|
||||
size: 80,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Aucun membre trouvé',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Modifiez vos critères de recherche ou ajoutez de nouveaux membres',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _addMember,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text('Ajouter un membre'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _showFilterSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => MembersFilterSheet(
|
||||
selectedFilter: _selectedFilter,
|
||||
onFilterChanged: (filter) {
|
||||
setState(() {
|
||||
_selectedFilter = filter;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMenuSelection(String value) {
|
||||
switch (value) {
|
||||
case 'export':
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Export des membres - En développement'),
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'import':
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Import des membres - En développement'),
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'stats':
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Statistiques détaillées - En développement'),
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshMembers() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Liste des membres actualisée'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMemberDetails(Map<String, dynamic> member) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Détails de ${member['firstName']} ${member['lastName']} - En développement'),
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editMember(Map<String, dynamic> member) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Édition de ${member['firstName']} ${member['lastName']} - En développement'),
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addMember() {
|
||||
HapticFeedback.lightImpact();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Ajouter un membre - En développement'),
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
final TabBar tabBar;
|
||||
|
||||
_TabBarDelegate(this.tabBar);
|
||||
|
||||
@override
|
||||
double get minExtent => tabBar.preferredSize.height;
|
||||
|
||||
@override
|
||||
double get maxExtent => tabBar.preferredSize.height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: tabBar,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_TabBarDelegate oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,995 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../core/error/error_handler.dart';
|
||||
import '../../../../core/validation/form_validator.dart';
|
||||
import '../../../../core/feedback/user_feedback.dart';
|
||||
import '../../../../core/animations/loading_animations.dart';
|
||||
import '../../../../core/animations/page_transitions.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/custom_text_field.dart';
|
||||
import '../../../../shared/widgets/buttons/buttons.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
|
||||
|
||||
/// Page de création d'un nouveau membre
|
||||
class MembreCreatePage extends StatefulWidget {
|
||||
const MembreCreatePage({super.key});
|
||||
|
||||
@override
|
||||
State<MembreCreatePage> createState() => _MembreCreatePageState();
|
||||
}
|
||||
|
||||
class _MembreCreatePageState extends State<MembreCreatePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late MembresBloc _membresBloc;
|
||||
late TabController _tabController;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers pour les champs du formulaire
|
||||
final _nomController = TextEditingController();
|
||||
final _prenomController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _telephoneController = TextEditingController();
|
||||
final _adresseController = TextEditingController();
|
||||
final _villeController = TextEditingController();
|
||||
final _codePostalController = TextEditingController();
|
||||
final _paysController = TextEditingController();
|
||||
final _professionController = TextEditingController();
|
||||
final _numeroMembreController = TextEditingController();
|
||||
|
||||
// Variables d'état
|
||||
DateTime? _dateNaissance;
|
||||
DateTime _dateAdhesion = DateTime.now();
|
||||
bool _actif = true;
|
||||
bool _isLoading = false;
|
||||
int _currentStep = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
// Générer un numéro de membre automatique
|
||||
_generateMemberNumber();
|
||||
|
||||
// Initialiser les valeurs par défaut
|
||||
_paysController.text = 'Côte d\'Ivoire';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_nomController.dispose();
|
||||
_prenomController.dispose();
|
||||
_emailController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_adresseController.dispose();
|
||||
_villeController.dispose();
|
||||
_codePostalController.dispose();
|
||||
_paysController.dispose();
|
||||
_professionController.dispose();
|
||||
_numeroMembreController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _generateMemberNumber() {
|
||||
final now = DateTime.now();
|
||||
final year = now.year.toString().substring(2);
|
||||
final month = now.month.toString().padLeft(2, '0');
|
||||
final random = (DateTime.now().millisecondsSinceEpoch % 1000).toString().padLeft(3, '0');
|
||||
_numeroMembreController.text = 'MBR$year$month$random';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: _buildAppBar(),
|
||||
body: BlocConsumer<MembresBloc, MembresState>(
|
||||
listener: (context, state) {
|
||||
if (state is MembreCreated) {
|
||||
// Fermer l'indicateur de chargement
|
||||
UserFeedback.hideLoading(context);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Afficher le message de succès avec feedback haptique
|
||||
UserFeedback.showSuccess(
|
||||
context,
|
||||
'Membre créé avec succès !',
|
||||
onAction: () => Navigator.of(context).pop(true),
|
||||
actionLabel: 'Voir la liste',
|
||||
);
|
||||
|
||||
// Retourner à la liste après un délai
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
});
|
||||
|
||||
} else if (state is MembresError) {
|
||||
// Fermer l'indicateur de chargement
|
||||
UserFeedback.hideLoading(context);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Gérer l'erreur avec le nouveau système
|
||||
ErrorHandler.handleError(
|
||||
context,
|
||||
state.failure,
|
||||
onRetry: () => _submitForm(),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildProgressIndicator(),
|
||||
Expanded(
|
||||
child: _buildFormContent(),
|
||||
),
|
||||
_buildBottomActions(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
title: const Text(
|
||||
'Nouveau membre',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
onPressed: _showHelp,
|
||||
tooltip: 'Aide',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressIndicator() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_buildStepIndicator(0, 'Informations\npersonnelles', Icons.person),
|
||||
_buildStepConnector(0),
|
||||
_buildStepIndicator(1, 'Contact &\nAdresse', Icons.contact_mail),
|
||||
_buildStepConnector(1),
|
||||
_buildStepIndicator(2, 'Finalisation', Icons.check_circle),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: (_currentStep + 1) / 3,
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepIndicator(int step, String label, IconData icon) {
|
||||
final isActive = step == _currentStep;
|
||||
final isCompleted = step < _currentStep;
|
||||
|
||||
Color color;
|
||||
if (isCompleted) {
|
||||
color = AppTheme.successColor;
|
||||
} else if (isActive) {
|
||||
color = AppTheme.primaryColor;
|
||||
} else {
|
||||
color = AppTheme.textHint;
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isCompleted ? AppTheme.successColor :
|
||||
isActive ? AppTheme.primaryColor : AppTheme.backgroundLight,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color, width: 2),
|
||||
),
|
||||
child: Icon(
|
||||
isCompleted ? Icons.check : icon,
|
||||
color: isCompleted || isActive ? Colors.white : color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: color,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepConnector(int step) {
|
||||
final isCompleted = step < _currentStep;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
margin: const EdgeInsets.only(bottom: 32),
|
||||
color: isCompleted ? AppTheme.successColor : AppTheme.backgroundLight,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormContent() {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: PageView(
|
||||
controller: PageController(initialPage: _currentStep),
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentStep = index;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
_buildPersonalInfoStep(),
|
||||
_buildContactStep(),
|
||||
_buildFinalizationStep(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPersonalInfoStep() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informations personnelles',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Renseignez les informations de base du nouveau membre',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Numéro de membre (généré automatiquement)
|
||||
CustomTextField(
|
||||
controller: _numeroMembreController,
|
||||
label: 'Numéro de membre',
|
||||
prefixIcon: Icons.badge,
|
||||
enabled: false,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Le numéro de membre est requis';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nom et Prénom
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _prenomController,
|
||||
label: 'Prénom *',
|
||||
hintText: 'Jean',
|
||||
prefixIcon: Icons.person_outline,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Le prénom est requis';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return 'Le prénom doit contenir au moins 2 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _nomController,
|
||||
label: 'Nom *',
|
||||
hintText: 'Dupont',
|
||||
prefixIcon: Icons.person_outline,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Le nom est requis';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return 'Le nom doit contenir au moins 2 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date de naissance
|
||||
InkWell(
|
||||
onTap: _selectDateNaissance,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppTheme.borderColor),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cake_outlined, color: AppTheme.textSecondary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Date de naissance',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_dateNaissance != null
|
||||
? DateFormat('dd/MM/yyyy').format(_dateNaissance!)
|
||||
: 'Sélectionner une date',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: _dateNaissance != null
|
||||
? AppTheme.textPrimary
|
||||
: AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.calendar_today, color: AppTheme.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Profession
|
||||
CustomTextField(
|
||||
controller: _professionController,
|
||||
label: 'Profession',
|
||||
hintText: 'Enseignant, Commerçant, etc.',
|
||||
prefixIcon: Icons.work_outline,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactStep() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Contact & Adresse',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Informations de contact et adresse du membre',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Email
|
||||
CustomTextField(
|
||||
controller: _emailController,
|
||||
label: 'Email *',
|
||||
hintText: 'exemple@email.com',
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'L\'email est requis';
|
||||
}
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
||||
return 'Format d\'email invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Téléphone
|
||||
CustomTextField(
|
||||
controller: _telephoneController,
|
||||
label: 'Téléphone *',
|
||||
hintText: '+225 XX XX XX XX XX',
|
||||
prefixIcon: Icons.phone_outlined,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9+\-\s\(\)]')),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Le téléphone est requis';
|
||||
}
|
||||
if (value.trim().length < 8) {
|
||||
return 'Numéro de téléphone invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Section Adresse
|
||||
const Text(
|
||||
'Adresse',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Adresse
|
||||
CustomTextField(
|
||||
controller: _adresseController,
|
||||
label: 'Adresse',
|
||||
hintText: 'Rue, quartier, etc.',
|
||||
prefixIcon: Icons.location_on_outlined,
|
||||
textInputAction: TextInputAction.next,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Ville et Code postal
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CustomTextField(
|
||||
controller: _villeController,
|
||||
label: 'Ville',
|
||||
hintText: 'Abidjan',
|
||||
prefixIcon: Icons.location_city_outlined,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _codePostalController,
|
||||
label: 'Code postal',
|
||||
hintText: '00225',
|
||||
prefixIcon: Icons.markunread_mailbox_outlined,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Pays
|
||||
CustomTextField(
|
||||
controller: _paysController,
|
||||
label: 'Pays',
|
||||
prefixIcon: Icons.flag_outlined,
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFinalizationStep() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Finalisation',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Vérifiez les informations et finalisez la création',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Résumé des informations
|
||||
_buildSummaryCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Date d'adhésion
|
||||
InkWell(
|
||||
onTap: _selectDateAdhesion,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppTheme.borderColor),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today_outlined, color: AppTheme.textSecondary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Date d\'adhésion',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy').format(_dateAdhesion),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.edit, color: AppTheme.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Statut actif
|
||||
SwitchListTile(
|
||||
title: const Text('Membre actif'),
|
||||
subtitle: const Text('Le membre peut accéder aux services'),
|
||||
value: _actif,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_actif = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.summarize, color: AppTheme.primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Résumé des informations',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSummaryRow('Nom complet', '${_prenomController.text} ${_nomController.text}'),
|
||||
_buildSummaryRow('Email', _emailController.text),
|
||||
_buildSummaryRow('Téléphone', _telephoneController.text),
|
||||
if (_dateNaissance != null)
|
||||
_buildSummaryRow('Date de naissance', DateFormat('dd/MM/yyyy').format(_dateNaissance!)),
|
||||
if (_professionController.text.isNotEmpty)
|
||||
_buildSummaryRow('Profession', _professionController.text),
|
||||
if (_adresseController.text.isNotEmpty)
|
||||
_buildSummaryRow('Adresse', _adresseController.text),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryRow(String label, String value) {
|
||||
if (value.trim().isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomActions() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_currentStep > 0)
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _previousStep,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.primaryColor,
|
||||
side: const BorderSide(color: AppTheme.primaryColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Précédent'),
|
||||
),
|
||||
),
|
||||
if (_currentStep > 0) const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: _currentStep == 0 ? 1 : 1,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleNextOrSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(_currentStep == 2 ? 'Créer le membre' : 'Suivant'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _previousStep() {
|
||||
if (_currentStep > 0) {
|
||||
setState(() {
|
||||
_currentStep--;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNextOrSubmit() {
|
||||
if (_currentStep < 2) {
|
||||
if (_validateCurrentStep()) {
|
||||
setState(() {
|
||||
_currentStep++;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_submitForm();
|
||||
}
|
||||
}
|
||||
|
||||
bool _validateCurrentStep() {
|
||||
switch (_currentStep) {
|
||||
case 0:
|
||||
return _validatePersonalInfo();
|
||||
case 1:
|
||||
return _validateContactInfo();
|
||||
case 2:
|
||||
return true; // Pas de validation spécifique pour la finalisation
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _validatePersonalInfo() {
|
||||
final errors = <String>[];
|
||||
|
||||
// Validation du prénom
|
||||
final prenomError = FormValidator.name(_prenomController.text, fieldName: 'Le prénom');
|
||||
if (prenomError != null) errors.add(prenomError);
|
||||
|
||||
// Validation du nom
|
||||
final nomError = FormValidator.name(_nomController.text, fieldName: 'Le nom');
|
||||
if (nomError != null) errors.add(nomError);
|
||||
|
||||
// Validation de la date de naissance
|
||||
if (_dateNaissance != null) {
|
||||
final dateError = FormValidator.birthDate(_dateNaissance!, minAge: 16);
|
||||
if (dateError != null) errors.add(dateError);
|
||||
}
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
UserFeedback.showWarning(context, errors.first);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _validateContactInfo() {
|
||||
final errors = <String>[];
|
||||
|
||||
// Validation de l'email
|
||||
final emailError = FormValidator.email(_emailController.text);
|
||||
if (emailError != null) errors.add(emailError);
|
||||
|
||||
// Validation du téléphone
|
||||
final phoneError = FormValidator.phone(_telephoneController.text);
|
||||
if (phoneError != null) errors.add(phoneError);
|
||||
|
||||
// Validation de l'adresse (optionnelle)
|
||||
final addressError = FormValidator.address(_adresseController.text);
|
||||
if (addressError != null) errors.add(addressError);
|
||||
|
||||
// Validation de la profession (optionnelle)
|
||||
final professionError = FormValidator.profession(_professionController.text);
|
||||
if (professionError != null) errors.add(professionError);
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
UserFeedback.showWarning(context, errors.first);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _submitForm() {
|
||||
// Validation finale complète
|
||||
if (!_validateAllSteps()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
UserFeedback.showWarning(context, 'Veuillez corriger les erreurs dans le formulaire');
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher l'indicateur de chargement
|
||||
UserFeedback.showLoading(context, message: 'Création du membre en cours...');
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Créer le modèle membre avec validation des données
|
||||
final membre = MembreModel(
|
||||
id: '', // Sera généré par le backend
|
||||
numeroMembre: _numeroMembreController.text.trim(),
|
||||
nom: _nomController.text.trim(),
|
||||
prenom: _prenomController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
telephone: _telephoneController.text.trim(),
|
||||
dateNaissance: _dateNaissance,
|
||||
adresse: _adresseController.text.trim().isNotEmpty ? _adresseController.text.trim() : null,
|
||||
ville: _villeController.text.trim().isNotEmpty ? _villeController.text.trim() : null,
|
||||
codePostal: _codePostalController.text.trim().isNotEmpty ? _codePostalController.text.trim() : null,
|
||||
pays: _paysController.text.trim().isNotEmpty ? _paysController.text.trim() : null,
|
||||
profession: _professionController.text.trim().isNotEmpty ? _professionController.text.trim() : null,
|
||||
dateAdhesion: _dateAdhesion,
|
||||
actif: _actif,
|
||||
statut: 'ACTIF',
|
||||
version: 1,
|
||||
dateCreation: DateTime.now(),
|
||||
);
|
||||
|
||||
// Envoyer l'événement de création
|
||||
_membresBloc.add(CreateMembre(membre));
|
||||
} catch (e) {
|
||||
UserFeedback.hideLoading(context);
|
||||
ErrorHandler.handleError(context, e, customMessage: 'Erreur lors de la préparation des données');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _validateAllSteps() {
|
||||
// Valider toutes les étapes
|
||||
if (!_validatePersonalInfo()) return false;
|
||||
if (!_validateContactInfo()) return false;
|
||||
|
||||
// Validation supplémentaire pour les champs obligatoires
|
||||
if (_dateNaissance == null) {
|
||||
UserFeedback.showWarning(context, 'La date de naissance est requise');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _selectDateNaissance() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateNaissance ?? DateTime.now().subtract(const Duration(days: 365 * 25)),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
locale: const Locale('fr', 'FR'),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_dateNaissance = date;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectDateAdhesion() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateAdhesion,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
locale: const Locale('fr', 'FR'),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_dateAdhesion = date;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showHelp() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Aide - Création de membre'),
|
||||
content: const SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Étapes de création :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('1. Informations personnelles : Nom, prénom, date de naissance'),
|
||||
Text('2. Contact & Adresse : Email, téléphone, adresse'),
|
||||
Text('3. Finalisation : Vérification et validation'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Champs obligatoires :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('• Nom et prénom'),
|
||||
Text('• Email (format valide)'),
|
||||
Text('• Téléphone'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Le numéro de membre est généré automatiquement selon le format : MBR + Année + Mois + Numéro séquentiel',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
import '../widgets/membre_info_section.dart';
|
||||
import '../widgets/membre_stats_section.dart';
|
||||
import '../widgets/membre_cotisations_section.dart';
|
||||
import '../widgets/membre_actions_section.dart';
|
||||
import '../widgets/membre_delete_dialog.dart';
|
||||
import 'membre_edit_page.dart';
|
||||
|
||||
/// Page de détails complète d'un membre
|
||||
class MembreDetailsPage extends StatefulWidget {
|
||||
const MembreDetailsPage({
|
||||
super.key,
|
||||
required this.membreId,
|
||||
this.membre,
|
||||
});
|
||||
|
||||
final String membreId;
|
||||
final MembreModel? membre;
|
||||
|
||||
@override
|
||||
State<MembreDetailsPage> createState() => _MembreDetailsPageState();
|
||||
}
|
||||
|
||||
class _MembreDetailsPageState extends State<MembreDetailsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late MembresBloc _membresBloc;
|
||||
late TabController _tabController;
|
||||
|
||||
MembreModel? _currentMembre;
|
||||
List<CotisationModel> _cotisations = [];
|
||||
bool _isLoadingCotisations = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_currentMembre = widget.membre;
|
||||
|
||||
// Charger les détails du membre si pas fourni
|
||||
if (_currentMembre == null) {
|
||||
_membresBloc.add(LoadMembreById(widget.membreId));
|
||||
}
|
||||
|
||||
// Charger les cotisations du membre
|
||||
_loadMemberCotisations();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadMemberCotisations() async {
|
||||
setState(() {
|
||||
_isLoadingCotisations = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Implémenter le chargement des cotisations via le repository
|
||||
// final cotisations = await getIt<CotisationRepository>()
|
||||
// .getCotisationsByMembre(widget.membreId);
|
||||
// setState(() {
|
||||
// _cotisations = cotisations;
|
||||
// });
|
||||
|
||||
// Simulation temporaire
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
setState(() {
|
||||
_cotisations = _generateMockCotisations();
|
||||
});
|
||||
} catch (e) {
|
||||
// Gérer l'erreur
|
||||
debugPrint('Erreur lors du chargement des cotisations: $e');
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoadingCotisations = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<CotisationModel> _generateMockCotisations() {
|
||||
// Données de test temporaires
|
||||
return [
|
||||
CotisationModel(
|
||||
id: '1',
|
||||
numeroReference: 'COT-2025-001',
|
||||
membreId: widget.membreId,
|
||||
typeCotisation: 'MENSUELLE',
|
||||
periode: 'Janvier 2025',
|
||||
montantDu: 25000,
|
||||
montantPaye: 25000,
|
||||
codeDevise: 'XOF',
|
||||
statut: 'PAYEE',
|
||||
dateEcheance: DateTime(2025, 1, 31),
|
||||
datePaiement: DateTime(2025, 1, 15),
|
||||
annee: 2025,
|
||||
recurrente: true,
|
||||
nombreRappels: 0,
|
||||
dateCreation: DateTime(2025, 1, 1),
|
||||
),
|
||||
CotisationModel(
|
||||
id: '2',
|
||||
numeroReference: 'COT-2025-002',
|
||||
membreId: widget.membreId,
|
||||
typeCotisation: 'MENSUELLE',
|
||||
periode: 'Février 2025',
|
||||
montantDu: 25000,
|
||||
montantPaye: 0,
|
||||
codeDevise: 'XOF',
|
||||
statut: 'EN_ATTENTE',
|
||||
dateEcheance: DateTime(2025, 2, 28),
|
||||
annee: 2025,
|
||||
recurrente: true,
|
||||
nombreRappels: 1,
|
||||
dateCreation: DateTime(2025, 2, 1),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
body: BlocConsumer<MembresBloc, MembresState>(
|
||||
listener: (context, state) {
|
||||
if (state is MembreLoaded) {
|
||||
setState(() {
|
||||
_currentMembre = state.membre;
|
||||
});
|
||||
} else if (state is MembresError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoading && _currentMembre == null) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Chargement des détails...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MembresError && _currentMembre == null) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, size: 64, color: AppTheme.errorColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _membresBloc.add(LoadMembreById(widget.membreId)),
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_currentMembre == null) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.person_off, size: 64),
|
||||
SizedBox(height: 16),
|
||||
Text('Membre non trouvé'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildContent();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
_buildAppBar(innerBoxIsScrolled),
|
||||
_buildMemberHeader(),
|
||||
_buildTabBar(),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildInfoTab(),
|
||||
_buildCotisationsTab(),
|
||||
_buildStatsTab(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(bool innerBoxIsScrolled) {
|
||||
return SliverAppBar(
|
||||
expandedHeight: 0,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(
|
||||
_currentMembre?.nomComplet ?? 'Détails du membre',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: _editMember,
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: _handleMenuAction,
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'call',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.phone),
|
||||
title: Text('Appeler'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'message',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.message),
|
||||
title: Text('Message'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.download),
|
||||
title: Text('Exporter'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.red),
|
||||
title: Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMemberHeader() {
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: AppTheme.primaryColor,
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
||||
child: MembreInfoSection(
|
||||
membre: _currentMembre!,
|
||||
showActions: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _TabBarDelegate(
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppTheme.primaryColor,
|
||||
unselectedLabelColor: AppTheme.textSecondary,
|
||||
indicatorColor: AppTheme.primaryColor,
|
||||
indicatorWeight: 3,
|
||||
tabs: const [
|
||||
Tab(text: 'Informations'),
|
||||
Tab(text: 'Cotisations'),
|
||||
Tab(text: 'Statistiques'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
MembreInfoSection(
|
||||
membre: _currentMembre!,
|
||||
showActions: true,
|
||||
onEdit: _editMember,
|
||||
onCall: _callMember,
|
||||
onMessage: _messageMember,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
MembreActionsSection(
|
||||
membre: _currentMembre!,
|
||||
onEdit: _editMember,
|
||||
onDelete: _deleteMember,
|
||||
onExport: _exportMember,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCotisationsTab() {
|
||||
return MembreCotisationsSection(
|
||||
membre: _currentMembre!,
|
||||
cotisations: _cotisations,
|
||||
isLoading: _isLoadingCotisations,
|
||||
onRefresh: _loadMemberCotisations,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: MembreStatsSection(
|
||||
membre: _currentMembre!,
|
||||
cotisations: _cotisations,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editMember() async {
|
||||
if (widget.membre == null) return;
|
||||
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MembreEditPage(membre: widget.membre!),
|
||||
),
|
||||
);
|
||||
|
||||
// Si le membre a été modifié avec succès, recharger les données
|
||||
if (result == true) {
|
||||
_loadMemberCotisations();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Membre modifié avec succès !'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _callMember() {
|
||||
// TODO: Implémenter l'appel
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Appel - À implémenter')),
|
||||
);
|
||||
}
|
||||
|
||||
void _messageMember() {
|
||||
// TODO: Implémenter l'envoi de message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Message - À implémenter')),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteMember() async {
|
||||
if (widget.membre == null) return;
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => MembreDeleteDialog(membre: widget.membre!),
|
||||
);
|
||||
|
||||
// Si le membre a été supprimé/désactivé avec succès
|
||||
if (result == true && mounted) {
|
||||
// Retourner à la liste des membres
|
||||
Navigator.of(context).pop();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Membre traité avec succès !'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _exportMember() {
|
||||
// TODO: Implémenter l'export
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Export - À implémenter')),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMenuAction(String action) {
|
||||
switch (action) {
|
||||
case 'call':
|
||||
_callMember();
|
||||
break;
|
||||
case 'message':
|
||||
_messageMember();
|
||||
break;
|
||||
case 'export':
|
||||
_exportMember();
|
||||
break;
|
||||
case 'delete':
|
||||
_deleteMember();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TabBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
const _TabBarDelegate(this.tabBar);
|
||||
|
||||
final TabBar tabBar;
|
||||
|
||||
@override
|
||||
double get minExtent => tabBar.preferredSize.height;
|
||||
|
||||
@override
|
||||
double get maxExtent => tabBar.preferredSize.height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: tabBar,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,225 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
import '../widgets/dashboard/welcome_section_widget.dart';
|
||||
import '../widgets/dashboard/members_kpi_section_widget.dart';
|
||||
import '../widgets/dashboard/members_quick_actions_widget.dart';
|
||||
import '../widgets/dashboard/members_analytics_widget.dart';
|
||||
import '../widgets/dashboard/members_enhanced_list_widget.dart';
|
||||
import '../widgets/dashboard/members_recent_activities_widget.dart';
|
||||
import '../widgets/dashboard/members_advanced_filters_widget.dart';
|
||||
import '../widgets/dashboard/members_smart_search_widget.dart';
|
||||
import '../widgets/dashboard/members_notifications_widget.dart';
|
||||
import 'membre_edit_page.dart';
|
||||
|
||||
// Import de l'architecture unifiée pour amélioration progressive
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
// Imports des optimisations de performance
|
||||
import '../../../../core/performance/performance_optimizer.dart';
|
||||
import '../../../../shared/widgets/performance/optimized_list_view.dart';
|
||||
|
||||
class MembresDashboardPage extends StatefulWidget {
|
||||
const MembresDashboardPage({super.key});
|
||||
|
||||
@override
|
||||
State<MembresDashboardPage> createState() => _MembresDashboardPageState();
|
||||
}
|
||||
|
||||
class _MembresDashboardPageState extends State<MembresDashboardPage> {
|
||||
late MembresBloc _membresBloc;
|
||||
Map<String, dynamic> _currentFilters = {};
|
||||
String _currentSearchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
|
||||
void _onFiltersChanged(Map<String, dynamic> filters) {
|
||||
setState(() {
|
||||
_currentFilters = filters;
|
||||
});
|
||||
// TODO: Appliquer les filtres aux données
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
setState(() {
|
||||
_currentSearchQuery = query;
|
||||
});
|
||||
// TODO: Appliquer la recherche
|
||||
if (query.isNotEmpty) {
|
||||
_membresBloc.add(SearchMembres(query));
|
||||
} else {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
void _onSuggestionSelected(Map<String, dynamic> suggestion) {
|
||||
switch (suggestion['type']) {
|
||||
case 'quick_filter':
|
||||
_onFiltersChanged(suggestion['filter']);
|
||||
break;
|
||||
case 'member':
|
||||
// TODO: Naviguer vers les détails du membre
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
// Utilisation de UnifiedPageLayout pour améliorer la cohérence
|
||||
// tout en conservant TOUS les widgets spécialisés existants
|
||||
return UnifiedPageLayout(
|
||||
title: 'Dashboard Membres',
|
||||
icon: Icons.people,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadData,
|
||||
tooltip: 'Actualiser',
|
||||
),
|
||||
],
|
||||
isLoading: state is MembresLoading,
|
||||
errorMessage: state is MembresError ? state.message : null,
|
||||
onRefresh: _loadData,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _loadData,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
tooltip: 'Actualiser les données',
|
||||
child: const Icon(Icons.refresh, color: Colors.white),
|
||||
),
|
||||
body: _buildDashboard(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDashboard() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section d'accueil
|
||||
const MembersWelcomeSectionWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Notifications en temps réel
|
||||
const MembersNotificationsWidget(),
|
||||
|
||||
// Recherche intelligente
|
||||
MembersSmartSearchWidget(
|
||||
onSearch: _onSearchChanged,
|
||||
onSuggestionSelected: _onSuggestionSelected,
|
||||
recentSearches: const [], // TODO: Implémenter l'historique
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtres avancés
|
||||
MembersAdvancedFiltersWidget(
|
||||
onFiltersChanged: _onFiltersChanged,
|
||||
initialFilters: _currentFilters,
|
||||
),
|
||||
|
||||
// KPI Cards
|
||||
const MembersKPISectionWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions rapides
|
||||
const MembersQuickActionsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Graphiques et analyses
|
||||
const MembersAnalyticsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Activités récentes
|
||||
const MembersRecentActivitiesWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Liste des membres améliorée
|
||||
BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoaded) {
|
||||
return MembersEnhancedListWidget(
|
||||
members: state.membres,
|
||||
onMemberTap: (member) {
|
||||
// TODO: Naviguer vers les détails du membre
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Détails de ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberCall: (member) {
|
||||
// TODO: Appeler le membre
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Appel de ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberMessage: (member) {
|
||||
// TODO: Envoyer un message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Message à ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberEdit: (member) async {
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MembreEditPage(membre: member),
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
// Recharger les données si le membre a été modifié
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
},
|
||||
searchQuery: _currentSearchQuery,
|
||||
filters: _currentFilters,
|
||||
);
|
||||
} else if (state is MembresLoading) {
|
||||
return MembersEnhancedListWidget(
|
||||
members: const [],
|
||||
onMemberTap: (member) {},
|
||||
isLoading: true,
|
||||
searchQuery: '',
|
||||
filters: const {},
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: Text('Erreur lors du chargement des membres'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/unified_components.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
|
||||
/// Dashboard des membres UnionFlow - Version Unifiée
|
||||
///
|
||||
/// Utilise l'architecture unifiée pour une expérience cohérente :
|
||||
/// - Composants standardisés réutilisables
|
||||
/// - Interface homogène avec les autres onglets
|
||||
/// - Performance optimisée avec animations fluides
|
||||
/// - Maintenabilité maximale
|
||||
class MembresDashboardPageUnified extends StatefulWidget {
|
||||
const MembresDashboardPageUnified({super.key});
|
||||
|
||||
@override
|
||||
State<MembresDashboardPageUnified> createState() => _MembresDashboardPageUnifiedState();
|
||||
}
|
||||
|
||||
class _MembresDashboardPageUnifiedState extends State<MembresDashboardPageUnified> {
|
||||
late MembresBloc _membresBloc;
|
||||
Map<String, dynamic> _currentFilters = {};
|
||||
String _currentSearchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
|
||||
void _onFiltersChanged(Map<String, dynamic> filters) {
|
||||
setState(() {
|
||||
_currentFilters = filters;
|
||||
});
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
setState(() {
|
||||
_currentSearchQuery = query;
|
||||
});
|
||||
if (query.isNotEmpty) {
|
||||
_membresBloc.add(SearchMembres(query));
|
||||
} else {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Membres',
|
||||
subtitle: 'Gestion des membres de l\'association',
|
||||
icon: Icons.people,
|
||||
iconColor: AppTheme.primaryColor,
|
||||
isLoading: state is MembresLoading,
|
||||
errorMessage: state is MembresError ? state.message : null,
|
||||
onRefresh: _loadData,
|
||||
actions: _buildActions(),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildKPISection(state),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildQuickActionsSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildFiltersSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
Expanded(child: _buildMembersList(state)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Actions de la barre d'outils
|
||||
List<Widget> _buildActions() {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers ajout membre
|
||||
},
|
||||
tooltip: 'Ajouter un membre',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.import_export),
|
||||
onPressed: () {
|
||||
// TODO: Import/Export des membres
|
||||
},
|
||||
tooltip: 'Import/Export',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.analytics),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers analyses détaillées
|
||||
},
|
||||
tooltip: 'Analyses',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Section de recherche intelligente
|
||||
Widget _buildSearchSection() {
|
||||
return UnifiedCard.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher un membre...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppTheme.backgroundLight,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
),
|
||||
if (_currentSearchQuery.isNotEmpty) ...[
|
||||
const SizedBox(height: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Recherche: "$_currentSearchQuery"',
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des KPI des membres
|
||||
Widget _buildKPISection(MembresState state) {
|
||||
final membres = state is MembresLoaded ? state.membres : <MembreModel>[];
|
||||
final totalMembres = membres.length;
|
||||
final membresActifs = membres.where((m) => m.statut == StatutMembre.actif).length;
|
||||
final nouveauxMembres = membres.where((m) {
|
||||
final now = DateTime.now();
|
||||
final monthAgo = DateTime(now.year, now.month - 1, now.day);
|
||||
return m.dateInscription.isAfter(monthAgo);
|
||||
}).length;
|
||||
final cotisationsAJour = membres.where((m) => m.cotisationAJour).length;
|
||||
|
||||
final kpis = [
|
||||
UnifiedKPIData(
|
||||
title: 'Total',
|
||||
value: totalMembres.toString(),
|
||||
icon: Icons.people,
|
||||
color: AppTheme.primaryColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: nouveauxMembres > 0 ? UnifiedKPITrendDirection.up : UnifiedKPITrendDirection.stable,
|
||||
value: '+$nouveauxMembres',
|
||||
label: 'ce mois',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Actifs',
|
||||
value: membresActifs.toString(),
|
||||
icon: Icons.verified_user,
|
||||
color: AppTheme.successColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.stable,
|
||||
value: '${((membresActifs / totalMembres) * 100).toInt()}%',
|
||||
label: 'du total',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Nouveaux',
|
||||
value: nouveauxMembres.toString(),
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.accentColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: 'Ce mois',
|
||||
label: 'inscriptions',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'À jour',
|
||||
value: '${((cotisationsAJour / totalMembres) * 100).toInt()}%',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: AppTheme.warningColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.stable,
|
||||
value: '$cotisationsAJour/$totalMembres',
|
||||
label: 'cotisations',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedKPISection(
|
||||
title: 'Statistiques des membres',
|
||||
kpis: kpis,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des actions rapides
|
||||
Widget _buildQuickActionsSection() {
|
||||
final actions = [
|
||||
UnifiedQuickAction(
|
||||
id: 'add_member',
|
||||
title: 'Nouveau\nMembre',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'bulk_import',
|
||||
title: 'Import\nGroupé',
|
||||
icon: Icons.upload_file,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'send_message',
|
||||
title: 'Message\nGroupé',
|
||||
icon: Icons.send,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'export_data',
|
||||
title: 'Exporter\nDonnées',
|
||||
icon: Icons.download,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'cotisations_reminder',
|
||||
title: 'Rappel\nCotisations',
|
||||
icon: Icons.notification_important,
|
||||
color: AppTheme.warningColor,
|
||||
badgeCount: 12,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'member_reports',
|
||||
title: 'Rapports\nMembres',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedQuickActionsSection(
|
||||
title: 'Actions rapides',
|
||||
actions: actions,
|
||||
onActionTap: _handleQuickAction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des filtres
|
||||
Widget _buildFiltersSection() {
|
||||
return UnifiedCard.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Filtres rapides',
|
||||
style: AppTheme.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingMedium),
|
||||
Wrap(
|
||||
spacing: AppTheme.spacingSmall,
|
||||
runSpacing: AppTheme.spacingSmall,
|
||||
children: [
|
||||
_buildFilterChip('Tous', _currentFilters.isEmpty),
|
||||
_buildFilterChip('Actifs', _currentFilters['statut'] == 'actif'),
|
||||
_buildFilterChip('Inactifs', _currentFilters['statut'] == 'inactif'),
|
||||
_buildFilterChip('Nouveaux', _currentFilters['type'] == 'nouveaux'),
|
||||
_buildFilterChip('Cotisations en retard', _currentFilters['cotisation'] == 'retard'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un chip de filtre
|
||||
Widget _buildFilterChip(String label, bool isSelected) {
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
Map<String, dynamic> newFilters = {};
|
||||
if (selected) {
|
||||
switch (label) {
|
||||
case 'Actifs':
|
||||
newFilters['statut'] = 'actif';
|
||||
break;
|
||||
case 'Inactifs':
|
||||
newFilters['statut'] = 'inactif';
|
||||
break;
|
||||
case 'Nouveaux':
|
||||
newFilters['type'] = 'nouveaux';
|
||||
break;
|
||||
case 'Cotisations en retard':
|
||||
newFilters['cotisation'] = 'retard';
|
||||
break;
|
||||
}
|
||||
}
|
||||
_onFiltersChanged(newFilters);
|
||||
},
|
||||
selectedColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
checkmarkColor: AppTheme.primaryColor,
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste des membres avec composant unifié
|
||||
Widget _buildMembersList(MembresState state) {
|
||||
if (state is MembresLoaded) {
|
||||
return UnifiedListWidget<MembreModel>(
|
||||
items: state.membres,
|
||||
itemBuilder: (context, membre, index) => _buildMemberCard(membre),
|
||||
isLoading: false,
|
||||
hasReachedMax: true,
|
||||
enableAnimations: true,
|
||||
emptyMessage: 'Aucun membre trouvé',
|
||||
emptyIcon: Icons.people_outline,
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Chargement des membres...'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte de membre
|
||||
Widget _buildMemberCard(MembreModel membre) {
|
||||
return UnifiedCard.listItem(
|
||||
onTap: () {
|
||||
// TODO: Navigation vers détails du membre
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
child: Text(
|
||||
membre.prenom.isNotEmpty ? membre.prenom[0].toUpperCase() : 'M',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingMedium),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${membre.prenom} ${membre.nom}',
|
||||
style: AppTheme.bodyLarge.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Text(
|
||||
membre.email,
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppTheme.spacingSmall,
|
||||
vertical: AppTheme.spacingXSmall,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(membre.statut).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(membre.statut),
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: _getStatusColor(membre.statut),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Icon(
|
||||
membre.cotisationAJour ? Icons.check_circle : Icons.warning,
|
||||
color: membre.cotisationAJour ? AppTheme.successColor : AppTheme.warningColor,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la couleur du statut
|
||||
Color _getStatusColor(StatutMembre statut) {
|
||||
switch (statut) {
|
||||
case StatutMembre.actif:
|
||||
return AppTheme.successColor;
|
||||
case StatutMembre.inactif:
|
||||
return AppTheme.errorColor;
|
||||
case StatutMembre.suspendu:
|
||||
return AppTheme.warningColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le libellé du statut
|
||||
String _getStatusLabel(StatutMembre statut) {
|
||||
switch (statut) {
|
||||
case StatutMembre.actif:
|
||||
return 'Actif';
|
||||
case StatutMembre.inactif:
|
||||
return 'Inactif';
|
||||
case StatutMembre.suspendu:
|
||||
return 'Suspendu';
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les actions rapides
|
||||
void _handleQuickAction(UnifiedQuickAction action) {
|
||||
switch (action.id) {
|
||||
case 'add_member':
|
||||
// TODO: Navigation vers ajout membre
|
||||
break;
|
||||
case 'bulk_import':
|
||||
// TODO: Import groupé
|
||||
break;
|
||||
case 'send_message':
|
||||
// TODO: Message groupé
|
||||
break;
|
||||
case 'export_data':
|
||||
// TODO: Export des données
|
||||
break;
|
||||
case 'cotisations_reminder':
|
||||
// TODO: Rappel cotisations
|
||||
break;
|
||||
case 'member_reports':
|
||||
// TODO: Rapports membres
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_membresBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,792 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../core/auth/services/permission_service.dart';
|
||||
import '../../../../core/services/communication_service.dart';
|
||||
import '../../../../core/services/export_import_service.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/permission_widget.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
import '../widgets/membre_card.dart';
|
||||
import '../widgets/membres_search_bar.dart';
|
||||
import '../widgets/membre_delete_dialog.dart';
|
||||
import '../widgets/membres_advanced_search.dart';
|
||||
import '../widgets/membres_export_dialog.dart';
|
||||
import '../widgets/membres_stats_overview.dart';
|
||||
import '../widgets/membres_view_controls.dart';
|
||||
import '../widgets/membre_enhanced_card.dart';
|
||||
import 'membre_details_page.dart';
|
||||
import 'membre_create_page.dart';
|
||||
import 'membre_edit_page.dart';
|
||||
import '../widgets/error_demo_widget.dart';
|
||||
|
||||
|
||||
/// Page de liste des membres avec fonctionnalités avancées
|
||||
class MembresListPage extends StatefulWidget {
|
||||
const MembresListPage({super.key});
|
||||
|
||||
@override
|
||||
State<MembresListPage> createState() => _MembresListPageState();
|
||||
}
|
||||
|
||||
class _MembresListPageState extends State<MembresListPage> with PermissionMixin {
|
||||
final RefreshController _refreshController = RefreshController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
late MembresBloc _membresBloc;
|
||||
List<MembreModel> _membres = [];
|
||||
|
||||
// Nouvelles variables pour les améliorations
|
||||
String _viewMode = 'card'; // 'card', 'list', 'grid'
|
||||
String _sortBy = 'name'; // 'name', 'date', 'age', 'status'
|
||||
bool _sortAscending = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Membres',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
// Recherche avancée - Accessible à tous les utilisateurs connectés
|
||||
PermissionIconButton(
|
||||
permission: () => permissionService.isAuthenticated,
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () => _showAdvancedSearch(),
|
||||
tooltip: 'Recherche avancée',
|
||||
),
|
||||
|
||||
// Export - Réservé aux gestionnaires et admins
|
||||
PermissionIconButton(
|
||||
permission: () => permissionService.canExportMembers,
|
||||
icon: const Icon(Icons.file_download),
|
||||
onPressed: () => _showExportDialog(),
|
||||
tooltip: 'Exporter',
|
||||
disabledMessage: 'Seuls les gestionnaires peuvent exporter les données',
|
||||
),
|
||||
|
||||
// Import - Réservé aux gestionnaires et admins
|
||||
PermissionIconButton(
|
||||
permission: () => permissionService.canCreateMembers,
|
||||
icon: const Icon(Icons.file_upload),
|
||||
onPressed: () => _showImportDialog(),
|
||||
tooltip: 'Importer',
|
||||
disabledMessage: 'Seuls les gestionnaires peuvent importer des données',
|
||||
),
|
||||
|
||||
// Statistiques - Réservé aux gestionnaires et admins
|
||||
PermissionIconButton(
|
||||
permission: () => permissionService.canViewMemberStats,
|
||||
icon: const Icon(Icons.analytics_outlined),
|
||||
onPressed: () => _showStatsDialog(),
|
||||
tooltip: 'Statistiques',
|
||||
disabledMessage: 'Seuls les gestionnaires peuvent voir les statistiques',
|
||||
),
|
||||
|
||||
// Démonstration des nouvelles fonctionnalités (développement uniquement)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
onPressed: () => _showErrorDemo(),
|
||||
tooltip: 'Démo Gestion d\'Erreurs',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
Container(
|
||||
color: AppTheme.primaryColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: MembresSearchBar(
|
||||
controller: _searchController,
|
||||
onSearch: (query) {
|
||||
_membresBloc.add(SearchMembres(query));
|
||||
},
|
||||
onClear: () {
|
||||
_searchController.clear();
|
||||
_membresBloc.add(const LoadMembres());
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des membres
|
||||
Expanded(
|
||||
child: BlocConsumer<MembresBloc, MembresState>(
|
||||
listener: (context, state) {
|
||||
if (state is MembresError) {
|
||||
_showErrorSnackBar(state.message);
|
||||
} else if (state is MembresErrorWithData) {
|
||||
_showErrorSnackBar(state.message);
|
||||
}
|
||||
|
||||
// Mettre à jour la liste des membres
|
||||
if (state is MembresLoaded) {
|
||||
_membres = state.membres;
|
||||
} else if (state is MembresErrorWithData) {
|
||||
_membres = state.membres;
|
||||
}
|
||||
|
||||
// Arrêter le refresh
|
||||
if (state is! MembresRefreshing && state is! MembresLoading) {
|
||||
_refreshController.refreshCompleted();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MembresError) {
|
||||
return _buildErrorWidget(state);
|
||||
}
|
||||
|
||||
if (state is MembresLoaded || state is MembresErrorWithData) {
|
||||
final membres = state is MembresLoaded
|
||||
? state.membres
|
||||
: (state as MembresErrorWithData).membres;
|
||||
|
||||
final isSearchResult = state is MembresLoaded
|
||||
? state.isSearchResult
|
||||
: (state as MembresErrorWithData).isSearchResult;
|
||||
|
||||
return SmartRefresher(
|
||||
controller: _refreshController,
|
||||
onRefresh: () => _membresBloc.add(const RefreshMembres()),
|
||||
header: const WaterDropHeader(
|
||||
waterDropColor: AppTheme.primaryColor,
|
||||
),
|
||||
child: membres.isEmpty
|
||||
? _buildEmptyWidget(isSearchResult)
|
||||
: _buildScrollableContent(membres),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucune donnée disponible',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: PermissionFAB(
|
||||
permission: () => permissionService.canCreateMembers,
|
||||
onPressed: () => _showAddMemberDialog(),
|
||||
tooltip: 'Ajouter un membre',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget d'erreur avec bouton de retry
|
||||
Widget _buildErrorWidget(MembresError state) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
state.isNetworkError ? Icons.wifi_off : Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppTheme.errorColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.isNetworkError
|
||||
? 'Problème de connexion'
|
||||
: 'Une erreur est survenue',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _membresBloc.add(const LoadMembres()),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget vide (aucun membre trouvé)
|
||||
Widget _buildEmptyWidget(bool isSearchResult) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isSearchResult ? Icons.search_off : Icons.people_outline,
|
||||
size: 64,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
isSearchResult
|
||||
? 'Aucun membre trouvé'
|
||||
: 'Aucun membre enregistré',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
isSearchResult
|
||||
? 'Essayez avec d\'autres termes de recherche'
|
||||
: 'Utilisez le bouton + en bas pour ajouter votre premier membre',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche une snackbar d'erreur
|
||||
void _showErrorSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
action: SnackBarAction(
|
||||
label: 'Fermer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche les détails d'un membre
|
||||
void _showMemberDetails(membre) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MembreDetailsPage(
|
||||
membreId: membre.id,
|
||||
membre: membre,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le contenu scrollable avec statistiques, contrôles et liste
|
||||
Widget _buildScrollableContent(List<MembreModel> membres) {
|
||||
final sortedMembers = _getSortedMembers(membres);
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// Widget de statistiques
|
||||
SliverToBoxAdapter(
|
||||
child: MembresStatsOverview(
|
||||
membres: membres,
|
||||
searchQuery: _searchController.text,
|
||||
),
|
||||
),
|
||||
|
||||
// Contrôles d'affichage
|
||||
SliverToBoxAdapter(
|
||||
child: MembresViewControls(
|
||||
viewMode: _viewMode,
|
||||
sortBy: _sortBy,
|
||||
sortAscending: _sortAscending,
|
||||
totalCount: membres.length,
|
||||
onViewModeChanged: (mode) {
|
||||
setState(() {
|
||||
_viewMode = mode;
|
||||
});
|
||||
},
|
||||
onSortChanged: (sortBy) {
|
||||
setState(() {
|
||||
_sortBy = sortBy;
|
||||
});
|
||||
},
|
||||
onSortDirectionChanged: () {
|
||||
setState(() {
|
||||
_sortAscending = !_sortAscending;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des membres en mode sliver
|
||||
_buildSliverMembersList(sortedMembers),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la liste des membres en mode sliver pour le scroll
|
||||
Widget _buildSliverMembersList(List<MembreModel> membres) {
|
||||
if (_viewMode == 'grid') {
|
||||
return SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.8,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: MembreEnhancedCard(
|
||||
membre: membres[index],
|
||||
viewMode: _viewMode,
|
||||
onTap: () => _showMemberDetails(membres[index]),
|
||||
onEdit: permissionService.canEditMembers
|
||||
? () => _showEditMemberDialog(membres[index])
|
||||
: null,
|
||||
onDelete: permissionService.canDeleteMembers
|
||||
? () => _showDeleteConfirmation(membres[index])
|
||||
: null,
|
||||
onCall: permissionService.canCallMembers
|
||||
? () => _callMember(membres[index])
|
||||
: null,
|
||||
onMessage: permissionService.canMessageMembers
|
||||
? () => _messageMember(membres[index])
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: membres.length,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: MembreEnhancedCard(
|
||||
membre: membres[index],
|
||||
viewMode: _viewMode,
|
||||
onTap: () => _showMemberDetails(membres[index]),
|
||||
onEdit: permissionService.canEditMembers
|
||||
? () => _showEditMemberDialog(membres[index])
|
||||
: null,
|
||||
onDelete: permissionService.canDeleteMembers
|
||||
? () => _showDeleteConfirmation(membres[index])
|
||||
: null,
|
||||
onCall: permissionService.canCallMembers
|
||||
? () => _callMember(membres[index])
|
||||
: null,
|
||||
onMessage: permissionService.canMessageMembers
|
||||
? () => _messageMember(membres[index])
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: membres.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trie les membres selon les critères sélectionnés
|
||||
List<MembreModel> _getSortedMembers(List<MembreModel> membres) {
|
||||
final sortedMembers = List<MembreModel>.from(membres);
|
||||
|
||||
sortedMembers.sort((a, b) {
|
||||
int comparison = 0;
|
||||
|
||||
switch (_sortBy) {
|
||||
case 'name':
|
||||
comparison = a.nomComplet.compareTo(b.nomComplet);
|
||||
break;
|
||||
case 'date':
|
||||
comparison = a.dateAdhesion.compareTo(b.dateAdhesion);
|
||||
break;
|
||||
case 'age':
|
||||
comparison = a.age.compareTo(b.age);
|
||||
break;
|
||||
case 'status':
|
||||
comparison = a.statut.compareTo(b.statut);
|
||||
break;
|
||||
}
|
||||
|
||||
return _sortAscending ? comparison : -comparison;
|
||||
});
|
||||
|
||||
return sortedMembers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Actions sur les membres
|
||||
Future<void> _callMember(MembreModel membre) async {
|
||||
// Vérifier les permissions
|
||||
if (!permissionService.canCallMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour appeler les membres');
|
||||
return;
|
||||
}
|
||||
|
||||
// Log de l'action pour audit
|
||||
permissionService.logAction('Tentative d\'appel membre', details: {
|
||||
'membreId': membre.id,
|
||||
'membreNom': membre.nomComplet,
|
||||
'telephone': membre.telephone,
|
||||
});
|
||||
|
||||
// Utiliser le service de communication pour effectuer l'appel
|
||||
final communicationService = CommunicationService();
|
||||
await communicationService.callMember(context, membre);
|
||||
}
|
||||
|
||||
Future<void> _messageMember(MembreModel membre) async {
|
||||
// Vérifier les permissions
|
||||
if (!permissionService.canMessageMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour envoyer des messages aux membres');
|
||||
return;
|
||||
}
|
||||
|
||||
// Log de l'action pour audit
|
||||
permissionService.logAction('Tentative d\'envoi SMS membre', details: {
|
||||
'membreId': membre.id,
|
||||
'membreNom': membre.nomComplet,
|
||||
'telephone': membre.telephone,
|
||||
});
|
||||
|
||||
// Utiliser le service de communication pour envoyer un SMS
|
||||
final communicationService = CommunicationService();
|
||||
await communicationService.sendSMS(context, membre);
|
||||
}
|
||||
|
||||
/// Affiche le formulaire d'ajout de membre
|
||||
void _showAddMemberDialog() async {
|
||||
// Vérifier les permissions avant d'ouvrir le formulaire
|
||||
if (!permissionService.canCreateMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour créer de nouveaux membres');
|
||||
return;
|
||||
}
|
||||
|
||||
permissionService.logAction('Ouverture formulaire création membre');
|
||||
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const MembreCreatePage(),
|
||||
),
|
||||
);
|
||||
|
||||
// Si un membre a été créé avec succès, recharger la liste
|
||||
if (result == true) {
|
||||
_membresBloc.add(const RefreshMembres());
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'édition de membre
|
||||
void _showEditMemberDialog(membre) async {
|
||||
// Vérifier les permissions avant d'ouvrir le formulaire
|
||||
if (!permissionService.canEditMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour modifier les membres');
|
||||
return;
|
||||
}
|
||||
|
||||
permissionService.logAction('Ouverture formulaire édition membre', details: {'membreId': membre.id});
|
||||
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MembreEditPage(membre: membre),
|
||||
),
|
||||
);
|
||||
|
||||
// Si le membre a été modifié avec succès, recharger la liste
|
||||
if (result == true) {
|
||||
_membresBloc.add(const RefreshMembres());
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche la confirmation de suppression
|
||||
void _showDeleteConfirmation(membre) async {
|
||||
// Vérifier les permissions avant d'ouvrir le dialog
|
||||
if (!permissionService.canDeleteMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour supprimer des membres');
|
||||
return;
|
||||
}
|
||||
|
||||
permissionService.logAction('Ouverture dialog suppression membre', details: {'membreId': membre.id});
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => MembreDeleteDialog(membre: membre),
|
||||
);
|
||||
|
||||
// Si le membre a été supprimé/désactivé avec succès, recharger la liste
|
||||
if (result == true) {
|
||||
_membresBloc.add(const RefreshMembres());
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche les statistiques
|
||||
void _showStatsDialog() {
|
||||
// Vérifier les permissions avant d'afficher les statistiques
|
||||
if (!permissionService.canViewMemberStats) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour voir les statistiques');
|
||||
return;
|
||||
}
|
||||
|
||||
permissionService.logAction('Consultation statistiques membres');
|
||||
|
||||
// TODO: Créer une page de statistiques détaillées
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Statistiques détaillées - En développement'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche la recherche avancée
|
||||
void _showAdvancedSearch() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) => MembresAdvancedSearch(
|
||||
onSearch: (filters) {
|
||||
// Fermer le modal
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Lancer la recherche avancée
|
||||
context.read<MembresBloc>().add(AdvancedSearchMembres(filters));
|
||||
|
||||
// Log de l'action pour audit
|
||||
permissionService.logAction('Recherche avancée membres', details: {
|
||||
'filtres': filters.keys.where((key) => filters[key] != null && filters[key].toString().isNotEmpty).toList(),
|
||||
'nombreFiltres': filters.values.where((value) => value != null && value.toString().isNotEmpty).length,
|
||||
});
|
||||
|
||||
// Afficher un message de confirmation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Recherche lancée avec ${filters.values.where((value) => value != null && value.toString().isNotEmpty).length} filtres'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'export
|
||||
void _showExportDialog() {
|
||||
// Vérifier les permissions avant d'ouvrir le dialog d'export
|
||||
if (!permissionService.canExportMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour exporter les données');
|
||||
return;
|
||||
}
|
||||
|
||||
permissionService.logAction('Ouverture dialog export membres', details: {'nombreMembres': _membres.length});
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => MembresExportDialog(
|
||||
membres: _membres,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'import
|
||||
Future<void> _showImportDialog() async {
|
||||
// Vérifier les permissions avant d'ouvrir le dialog d'import
|
||||
if (!permissionService.canCreateMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour importer des données');
|
||||
return;
|
||||
}
|
||||
|
||||
permissionService.logAction('Tentative import membres');
|
||||
|
||||
// Afficher un dialog de confirmation
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.file_upload,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Importer des membres',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Sélectionnez un fichier Excel (.xlsx), CSV (.csv) ou JSON (.json) contenant les données des membres à importer.',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Formats supportés :',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('• Excel (.xlsx)'),
|
||||
Text('• CSV (.csv)'),
|
||||
Text('• JSON (.json)'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'⚠️ Les données existantes ne seront pas supprimées. Les nouveaux membres seront ajoutés.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.warningColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
icon: const Icon(Icons.file_upload),
|
||||
label: const Text('Sélectionner fichier'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && mounted) {
|
||||
// Effectuer l'import
|
||||
final exportService = ExportImportService();
|
||||
final importedMembers = await exportService.importMembers(context);
|
||||
|
||||
if (importedMembers != null && importedMembers.isNotEmpty && mounted) {
|
||||
// Log de l'action réussie
|
||||
permissionService.logAction('Import membres réussi', details: {
|
||||
'nombreMembres': importedMembers.length,
|
||||
});
|
||||
|
||||
// TODO: Intégrer les membres importés avec l'API
|
||||
// Pour l'instant, on affiche juste un message de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.info, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${importedMembers.length} membres importés avec succès. Intégration avec l\'API en cours de développement.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche la page de démonstration des nouvelles fonctionnalités
|
||||
void _showErrorDemo() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ErrorDemoWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user