Refactoring

This commit is contained in:
DahoudG
2025-09-17 17:54:06 +00:00
parent 12d514d866
commit 63fe107f98
165 changed files with 54220 additions and 276 deletions

View File

@@ -132,8 +132,15 @@ class _MembreEditPageState extends State<MembreEditPage>
Widget build(BuildContext context) {
return BlocProvider.value(
value: _membresBloc,
child: WillPopScope(
onWillPop: _onWillPop,
child: PopScope(
canPop: !_hasChanges,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _onWillPop();
if (shouldPop && context.mounted) {
Navigator.of(context).pop();
}
},
child: Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: _buildAppBar(),

View File

@@ -14,6 +14,14 @@ 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});
@@ -73,87 +81,32 @@ class _MembresDashboardPageState extends State<MembresDashboardPage> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _membresBloc,
child: Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
title: const Text(
'Dashboard Membres',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
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,
tooltip: 'Actualiser',
backgroundColor: AppTheme.primaryColor,
tooltip: 'Actualiser les données',
child: const Icon(Icons.refresh, color: Colors.white),
),
],
),
body: BlocBuilder<MembresBloc, MembresState>(
builder: (context, state) {
if (state is MembresLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state is MembresError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AppTheme.errorColor,
),
const SizedBox(height: 16),
const Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
state.message,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadData,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
);
}
return _buildDashboard();
},
),
floatingActionButton: FloatingActionButton(
onPressed: _loadData,
backgroundColor: AppTheme.primaryColor,
tooltip: 'Actualiser les données',
child: const Icon(Icons.refresh, color: Colors.white),
),
body: _buildDashboard(),
);
},
),
);
}
@@ -234,14 +187,17 @@ class _MembresDashboardPageState extends State<MembresDashboardPage> {
),
);
},
onMemberEdit: (member) {
// TODO: Modifier le membre
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Modification de ${member.nomComplet}'),
backgroundColor: AppTheme.warningColor,
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,

View File

@@ -0,0 +1,488 @@
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

@@ -7,7 +7,6 @@ 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/coming_soon_page.dart';
import '../../../../shared/widgets/permission_widget.dart';
import '../bloc/membres_bloc.dart';
import '../bloc/membres_event.dart';
@@ -22,6 +21,7 @@ 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';
@@ -540,7 +540,7 @@ class _MembresListPageState extends State<MembresListPage> with PermissionMixin
}
/// Affiche le dialog d'édition de membre
void _showEditMemberDialog(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');
@@ -549,16 +549,16 @@ class _MembresListPageState extends State<MembresListPage> with PermissionMixin
permissionService.logAction('Ouverture formulaire édition membre', details: {'membreId': membre.id});
// TODO: Implémenter le formulaire d'édition
showDialog(
context: context,
builder: (context) => const ComingSoonPage(
title: 'Modifier le membre',
description: 'Le formulaire de modification sera bientôt disponible.',
icon: Icons.edit,
color: AppTheme.warningColor,
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