feat(ui): RefreshIndicator + AlwaysScrollable + dark mode sur 14 pages

RefreshIndicator ajouté (dispatche les events BLoC appropriés) :
- adhesion_detail, adhesions_page, demande_aide_detail, demandes_aide_page
- event_detail, organization_detail, org_selector, org_types
- user_management_detail, reports (TabBarView), logs (Dashboard tab)
- profile (onglet Perso), backup (3 onglets), notifications

Fixes associés :
- AlwaysScrollableScrollPhysics sur tous les scroll widgets
  (permet pull-to-refresh même si contenu < écran)
- Empty states des listes : wrappés dans SingleChildScrollView pour refresh
- Dark mode adaptatif sur textes/surfaces/borders hardcodés
- backup_page : bouton retour ajouté dans le header gradient
- org_types : chevron/star/border adaptatifs
- reports : couleurs placeholders graphique + chevrons
This commit is contained in:
dahoud
2026-04-15 20:13:50 +00:00
parent f78892e5f6
commit 55f84da49a
14 changed files with 1565 additions and 1538 deletions

View File

@@ -21,40 +21,52 @@ class UserManagementDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: const UFAppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Détail utilisateur',
moduleGradient: ModuleColors.systemeGradient,
),
body: BlocBuilder<AdminUsersBloc, AdminUsersState>(
builder: (context, state) {
if (state is AdminUsersLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is AdminUsersError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.message),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<AdminUsersBloc>().add(AdminUserDetailRequested(userId)),
child: const Text('Réessayer'),
),
],
),
);
}
if (state is AdminUserDetailLoaded) {
return _UserDetailContent(
user: state.user,
userRoles: state.userRoles,
allRoles: state.allRoles,
userId: userId,
);
}
return const SizedBox();
},
body: SafeArea(
top: false,
child: BlocBuilder<AdminUsersBloc, AdminUsersState>(
builder: (context, state) {
if (state is AdminUsersLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is AdminUsersError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
const SizedBox(height: 12),
Text(state.message, textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => context.read<AdminUsersBloc>()
.add(AdminUserDetailWithRolesRequested(userId)),
icon: const Icon(Icons.refresh, size: 16),
label: const Text('Réessayer'),
),
],
),
);
}
if (state is AdminUserDetailLoaded) {
return _UserDetailContent(
user: state.user,
userRoles: state.userRoles,
allRoles: state.allRoles,
userId: userId,
);
}
// AdminUserRolesUpdated : rechargement en cours — garder un indicateur léger
if (state is AdminUserRolesUpdated) {
return const Center(child: CircularProgressIndicator());
}
return const SizedBox();
},
),
),
);
}
@@ -96,7 +108,13 @@ class _UserDetailContentState extends State<_UserDetailContent> {
);
}
},
child: SingleChildScrollView(
child: RefreshIndicator(
color: ModuleColors.systeme,
onRefresh: () async => context
.read<AdminUsersBloc>()
.add(AdminUserDetailWithRolesRequested(widget.userId)),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -122,42 +140,96 @@ class _UserDetailContentState extends State<_UserDetailContent> {
),
),
const SizedBox(height: 8),
Text(
'RÔLES (SÉLECTION)',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
CoreCard(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.manage_accounts_outlined,
color: Theme.of(context).colorScheme.onSurfaceVariant, size: 16),
const SizedBox(width: 8),
Text(
'RÔLES',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Spacer(),
Text(
'${_selectedRoleNames.length} sélectionné(s)',
style: AppTypography.subtitleSmall.copyWith(
fontSize: 10,
color: AppColors.primary,
),
),
],
),
const SizedBox(height: 8),
if (widget.allRoles.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
const Icon(Icons.warning_amber_outlined,
size: 16, color: AppColors.error),
const SizedBox(width: 8),
Expanded(
child: Text(
'Impossible de charger les rôles disponibles.',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.error),
),
),
TextButton.icon(
onPressed: () => context.read<AdminUsersBloc>()
.add(AdminUserDetailWithRolesRequested(widget.userId)),
icon: const Icon(Icons.refresh, size: 14),
label: const Text('Recharger'),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
],
),
)
else
...widget.allRoles.map((role) {
final selected = _selectedRoleNames.contains(role.name);
return CheckboxListTile(
title: Text(role.name, style: AppTypography.bodyTextSmall),
subtitle: role.description != null && role.description!.isNotEmpty
? Text(role.description!,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10))
: null,
activeColor: AppColors.primary,
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
dense: true,
value: selected,
onChanged: (v) => setState(() {
if (v == true) {
_selectedRoleNames.add(role.name);
} else {
_selectedRoleNames.remove(role.name);
}
}),
);
}),
],
),
),
const SizedBox(height: 8),
...widget.allRoles.map((role) {
final selected = _selectedRoleNames.contains(role.name);
return CheckboxListTile(
title: Text(role.name, style: AppTypography.bodyTextSmall),
activeColor: AppColors.primaryGreen,
contentPadding: EdgeInsets.zero,
dense: true,
value: selected,
onChanged: (v) {
setState(() {
if (v == true) {
_selectedRoleNames.add(role.name);
} else {
_selectedRoleNames.remove(role.name);
}
});
},
);
}),
const SizedBox(height: 12),
UFPrimaryButton(
label: 'Enregistrer les rôles',
onPressed: () {
context.read<AdminUsersBloc>().add(
AdminUserRolesUpdateRequested(widget.userId, _selectedRoleNames.toList()),
);
},
),
if (widget.allRoles.isNotEmpty)
UFPrimaryButton(
label: 'Enregistrer les rôles',
onPressed: () => context.read<AdminUsersBloc>().add(
AdminUserRolesUpdateRequested(
widget.userId, _selectedRoleNames.toList())),
),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 8),
@@ -171,7 +243,7 @@ class _UserDetailContentState extends State<_UserDetailContent> {
const SizedBox(height: 8),
Text(
'Permet à cet utilisateur (ex. admin d\'organisation) de voir « Mes organisations » et d\'accéder au dashboard de l\'organisation.',
style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight),
style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
const SizedBox(height: 12),
OutlinedButton.icon(
@@ -181,13 +253,14 @@ class _UserDetailContentState extends State<_UserDetailContent> {
icon: const Icon(Icons.business, size: 18),
label: const Text('Associer à une organisation'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(color: AppColors.primaryGreen),
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary),
),
),
],
),
),
), // SingleChildScrollView
), // RefreshIndicator
);
}