Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -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();
}
}

View File

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