refactoring

This commit is contained in:
dahoud
2026-03-31 09:14:47 +00:00
parent 9bfffeeebe
commit 5383df6dcb
200 changed files with 11192 additions and 7063 deletions

View File

@@ -8,13 +8,14 @@ import '../widgets/organization_card.dart';
import '../widgets/create_organization_dialog.dart';
import '../widgets/edit_organization_dialog.dart';
import 'organization_detail_page.dart';
import 'org_types_page.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
import '../../../../shared/design_system/unionflow_design_v2.dart';
import '../../../../shared/design_system/components/animated_fade_in.dart';
import '../../../../shared/design_system/components/animated_slide_in.dart';
import '../../../../shared/design_system/components/african_pattern_background.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../../../features/authentication/data/models/user_role.dart';
/// Page de gestion des organisations - Interface sophistiquée et exhaustive
///
@@ -36,7 +37,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
final TextEditingController _searchController = TextEditingController();
TabController? _tabController;
final ScrollController _scrollController = ScrollController();
List<TypeOrganization?> _availableTypes = [];
List<String?> _availableTypes = [];
@override
void initState() {
@@ -62,21 +63,21 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
/// Calcule les types d'organisations disponibles dans les données
List<TypeOrganization?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
List<String?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
if (organizations.isEmpty) {
return [null]; // Seulement "Toutes"
}
// Extraire tous les types uniques
final typesSet = organizations.map((org) => org.typeOrganisation).toSet();
final types = typesSet.toList()..sort((a, b) => a.displayName.compareTo(b.displayName));
final types = typesSet.toList()..sort((a, b) => a.compareTo(b));
// null en premier pour "Toutes", puis les types triés alphabétiquement
return [null, ...types];
}
/// Initialise ou met à jour le TabController si les types ont changé
void _updateTabController(List<TypeOrganization?> newTypes) {
void _updateTabController(List<String?> newTypes) {
if (_availableTypes.length != newTypes.length ||
!_availableTypes.every((type) => newTypes.contains(type))) {
_availableTypes = newTypes;
@@ -94,7 +95,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: UnionFlowColors.error,
backgroundColor: AppColors.error,
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'Réessayer',
@@ -109,7 +110,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation créée avec succès'),
backgroundColor: UnionFlowColors.success,
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
@@ -117,7 +118,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation mise à jour avec succès'),
backgroundColor: UnionFlowColors.success,
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
@@ -125,7 +126,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation supprimée avec succès'),
backgroundColor: UnionFlowColors.success,
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
@@ -137,10 +138,21 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
backgroundColor: Colors.transparent,
appBar: UFAppBar(
title: 'Gestion des Organisations',
backgroundColor: UnionFlowColors.surface,
foregroundColor: UnionFlowColors.textPrimary,
backgroundColor: AppColors.lightSurface,
foregroundColor: AppColors.textPrimaryLight,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.category_outlined),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const OrgTypesPage(),
),
);
},
tooltip: 'Types d\'organisations',
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
@@ -246,7 +258,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
padding: EdgeInsets.all(SpacingTokens.md),
child: Center(
child: CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
color: AppColors.primaryGreen,
),
),
),
@@ -287,10 +299,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
if (state is! OrganizationsLoaded && state is! OrganizationsLoadingMore) {
return null;
}
// Réservé au Super Admin uniquement
final authState = context.read<AuthBloc>().state;
if (authState is! AuthAuthenticated ||
authState.effectiveRole != UserRole.superAdmin) {
return null;
}
return FloatingActionButton.extended(
onPressed: _showCreateOrganizationDialog,
backgroundColor: UnionFlowColors.unionGreen,
backgroundColor: AppColors.primaryGreen,
elevation: 8,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
@@ -308,9 +326,19 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
gradient: const LinearGradient(
colors: [AppColors.brandGreen, AppColors.primaryGreen],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.greenGlowShadow,
boxShadow: [
BoxShadow(
color: AppColors.primaryGreen.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
@@ -334,7 +362,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
const Text(
'Gestion des Organisations',
style: TextStyle(
fontSize: 20,
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
@@ -370,9 +398,9 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -381,16 +409,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
children: [
const Icon(
Icons.analytics_outlined,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
size: 20,
),
const SizedBox(width: SpacingTokens.xs),
const Text(
'Statistiques',
style: TextStyle(
fontSize: 16,
fontSize: 13,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
),
],
@@ -403,7 +431,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Total',
totalOrgs.toString(),
Icons.business_outlined,
UnionFlowColors.unionGreen,
AppColors.primaryGreen,
),
),
const SizedBox(width: SpacingTokens.sm),
@@ -412,7 +440,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Actives',
activeOrgs.toString(),
Icons.check_circle_outline,
UnionFlowColors.success,
AppColors.success,
),
),
const SizedBox(width: SpacingTokens.sm),
@@ -421,7 +449,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Membres',
totalMembers.toString(),
Icons.people_outline,
UnionFlowColors.info,
AppColors.primaryGreen,
),
),
],
@@ -452,14 +480,14 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
fontWeight: FontWeight.w500,
),
),
@@ -473,16 +501,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Container(
decoration: BoxDecoration(
color: UnionFlowColors.surfaceVariant,
color: AppColors.lightBackground,
borderRadius: BorderRadius.circular(RadiusTokens.md),
border: Border.all(
color: UnionFlowColors.border,
color: AppColors.lightBorder,
width: 1,
),
),
@@ -494,17 +522,17 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
decoration: InputDecoration(
hintText: 'Rechercher par nom, type, localisation...',
hintStyle: const TextStyle(
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
fontSize: 14,
),
prefixIcon: const Icon(Icons.search, color: UnionFlowColors.unionGreen),
prefixIcon: const Icon(Icons.search, color: AppColors.primaryGreen),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
context.read<OrganizationsBloc>().add(const SearchOrganizations(''));
},
icon: const Icon(Icons.clear, color: UnionFlowColors.textSecondary),
icon: const Icon(Icons.clear, color: AppColors.textSecondaryLight),
)
: null,
border: InputBorder.none,
@@ -519,7 +547,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
/// Onglets de catégories générés dynamiquement selon les types disponibles
Widget _buildCategoryTabs(List<TypeOrganization?> availableTypes) {
Widget _buildCategoryTabs(List<String?> availableTypes) {
if (_tabController == null || availableTypes.isEmpty) {
return const SizedBox.shrink();
}
@@ -528,16 +556,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
builder: (context, state) {
return Container(
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: TabBar(
controller: _tabController!,
isScrollable: availableTypes.length > 4, // Scrollable si plus de 4 types
labelColor: UnionFlowColors.unionGreen,
unselectedLabelColor: UnionFlowColors.textSecondary,
indicatorColor: UnionFlowColors.unionGreen,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicatorColor: AppColors.primaryGreen,
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: const TextStyle(
@@ -560,22 +588,8 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
},
tabs: availableTypes.map((type) {
// null = "Toutes", sinon utiliser le displayName du type
final label = type == null ? 'Toutes' : type.displayName;
final icon = type?.icon; // Emoji du type
return Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Text(icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: SpacingTokens.xs),
],
Text(label),
],
),
);
final label = type == null ? 'Toutes' : type;
return Tab(text: label);
}).toList(),
),
);
@@ -623,13 +637,13 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
Container(
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreenPale,
color: AppColors.primaryGreen.withOpacity(0.12),
shape: BoxShape.circle,
),
child: const Icon(
Icons.business_outlined,
size: 64,
color: UnionFlowColors.unionGreen,
color: AppColors.primaryGreen,
),
),
const SizedBox(height: SpacingTokens.lg),
@@ -638,7 +652,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
),
const SizedBox(height: SpacingTokens.xs),
@@ -646,7 +660,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Essayez de modifier vos critères de recherche\nou créez une nouvelle organisation',
style: TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
),
textAlign: TextAlign.center,
),
@@ -659,7 +673,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
icon: const Icon(Icons.clear_all),
label: const Text('Réinitialiser les filtres'),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
@@ -683,7 +697,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
color: AppColors.primaryGreen,
strokeWidth: 3,
),
const SizedBox(height: SpacingTokens.md),
@@ -691,7 +705,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Chargement des organisations...',
style: TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
),
),
],
@@ -706,10 +720,10 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
margin: const EdgeInsets.all(SpacingTokens.xl),
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
color: AppColors.error.withOpacity(0.08),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
border: Border.all(
color: UnionFlowColors.errorLight,
color: AppColors.error.withOpacity(0.3),
width: 1,
),
),
@@ -719,7 +733,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
const Icon(
Icons.error_outline,
size: 64,
color: UnionFlowColors.error,
color: AppColors.error,
),
const SizedBox(height: SpacingTokens.md),
Text(
@@ -727,7 +741,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
textAlign: TextAlign.center,
),
@@ -737,7 +751,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
state.details!,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
),
textAlign: TextAlign.center,
),
@@ -750,7 +764,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
@@ -771,10 +785,11 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
void _showOrganizationDetails(OrganizationModel org) {
final orgId = org.id;
if (orgId == null || orgId.isEmpty) return;
final bloc = context.read<OrganizationsBloc>();
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => BlocProvider.value(
value: context.read<OrganizationsBloc>(),
builder: (_) => BlocProvider.value(
value: bloc,
child: OrganizationDetailPage(organizationId: orgId),
),
),
@@ -782,39 +797,48 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
void _showCreateOrganizationDialog() {
final bloc = context.read<OrganizationsBloc>();
showDialog(
context: context,
builder: (context) => const CreateOrganizationDialog(),
builder: (_) => BlocProvider.value(
value: bloc,
child: const CreateOrganizationDialog(),
),
);
}
void _showEditOrganizationDialog(OrganizationModel org) {
final bloc = context.read<OrganizationsBloc>();
showDialog(
context: context,
builder: (context) => EditOrganizationDialog(organization: org),
builder: (_) => BlocProvider.value(
value: bloc,
child: EditOrganizationDialog(organization: org),
),
);
}
void _confirmDeleteOrganization(OrganizationModel org) {
final bloc = context.read<OrganizationsBloc>();
showDialog(
context: context,
builder: (context) => AlertDialog(
builder: (dialogContext) => AlertDialog(
title: const Text('Supprimer l\'organisation'),
content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (org.id != null) {
context.read<OrganizationsBloc>().add(DeleteOrganization(org.id!));
bloc.add(DeleteOrganization(org.id!));
}
Navigator.of(context).pop();
Navigator.of(dialogContext).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.error,
backgroundColor: AppColors.error,
foregroundColor: Colors.white,
),
child: const Text('Supprimer'),