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

@@ -58,7 +58,7 @@ class _ReportsPageState extends State<ReportsPage>
}
if (state is ReportsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.orange),
SnackBar(content: Text(state.message), backgroundColor: AppColors.warning),
);
}
if (state is ReportScheduled) {
@@ -74,184 +74,75 @@ class _ReportsPageState extends State<ReportsPage>
},
builder: (context, state) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
body: Column(
children: [
_buildHeader(),
_buildTabBar(),
if (state is ReportsLoading)
const LinearProgressIndicator(
minHeight: 2,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildOverviewTab(),
_buildMembersTab(),
_buildOrganizationsTab(),
_buildEventsTab(),
],
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Rapports & Analytics',
moduleGradient: ModuleColors.rapportsGradient,
actions: [
IconButton(
onPressed: () => _showExportDialog(),
icon: const Icon(Icons.file_download_outlined, size: 20),
tooltip: 'Exporter',
),
],
bottom: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
tabs: const [
Tab(child: Text('GLOBAL')),
Tab(child: Text('MEMBRES')),
Tab(child: Text('ORGS')),
Tab(child: Text('EVENTS')),
],
),
),
body: SafeArea(
top: false,
child: Column(
children: [
if (state is ReportsLoading)
const LinearProgressIndicator(
minHeight: 2,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
),
Expanded(
child: RefreshIndicator(
color: ModuleColors.rapports,
onRefresh: () async => context
.read<ReportsBloc>()
.add(const LoadDashboardReports()),
child: TabBarView(
controller: _tabController,
children: [
_buildOverviewTab(),
_buildMembersTab(),
_buildOrganizationsTab(),
_buildEventsTab(),
],
),
),
),
],
),
),
);
},
);
}
Widget _buildHeader() {
return Container(
width: double.infinity,
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 12,
bottom: 16,
left: 12,
right: 12,
),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.primaryGreen,
AppColors.brandGreen,
],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UnionFlow Analytics'.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
const SizedBox(height: 4),
const Text(
'Rapports & Insights',
style: AppTypography.headerSmall,
),
],
),
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: () => _showExportDialog(),
icon: const Icon(Icons.file_download_outlined, color: Colors.white),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHeaderStat(
'Membres',
_statsMembres['total']?.toString() ?? '...',
Icons.people_outline,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHeaderStat(
'Organisations',
_statsMembres['totalOrganisations']?.toString() ?? '...',
Icons.business_outlined,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHeaderStat(
'Événements',
_statsEvenements['total']?.toString() ?? '...',
Icons.event_outlined,
),
),
],
),
],
),
);
}
Widget _buildHeaderStat(String label, String value, IconData icon) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 18),
const SizedBox(height: 8),
Text(
value,
style: AppTypography.headerSmall.copyWith(fontSize: 18),
),
Text(
label.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.7),
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildTabBar() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppColors.lightBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)),
),
child: TabBar(
controller: _tabController,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicatorColor: AppColors.primaryGreen,
indicatorSize: TabBarIndicatorSize.label,
dividerColor: Colors.transparent,
labelStyle: AppTypography.badgeText.copyWith(fontWeight: FontWeight.bold),
tabs: const [
Tab(text: 'GLOBAL'),
Tab(text: 'MEMBRES'),
Tab(text: 'ORGS'),
Tab(text: 'EVENTS'),
],
),
);
}
// _buildHeader(), _buildHeaderStat() et _buildTabBar() supprimés :
// titre + action Export migrés dans UFAppBar, TabBar dans UFAppBar.bottom.
// Les KPIs sont toujours disponibles dans _buildKPICards() (onglet Global).
Widget _buildOverviewTab() {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
children: [
_buildKPICards(),
@@ -282,7 +173,7 @@ class _ReportsPageState extends State<ReportsPage>
const SizedBox(height: 8),
Row(
children: [
Expanded(child: _buildKPICard('Cotisations', totalCotisations, Icons.payments_outlined, AppColors.brandGreen)),
Expanded(child: _buildKPICard('Cotisations', totalCotisations, Icons.payments_outlined, AppColors.primaryDark)),
const SizedBox(width: 8),
Expanded(child: _buildKPICard('Événements', totalEvenements, Icons.event_available_outlined, AppColors.warning)),
],
@@ -332,7 +223,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [
Row(
children: [
const Icon(Icons.analytics_outlined, color: AppColors.primaryGreen, size: 20),
const Icon(Icons.analytics_outlined, color: AppColors.primary, size: 20),
const SizedBox(width: 8),
Text(
'Évolution de l\'Activité'.toUpperCase(),
@@ -341,26 +232,29 @@ class _ReportsPageState extends State<ReportsPage>
],
),
const SizedBox(height: 10),
Container(
height: 180,
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.auto_graph_outlined, color: AppColors.textSecondaryLight, size: 40),
SizedBox(height: 12),
Text(
'Visualisation graphique en préparation',
style: AppTypography.subtitleSmall,
),
],
Builder(builder: (context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
height: 180,
decoration: BoxDecoration(
color: (isDark ? AppColors.borderDark : AppColors.border).withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.auto_graph_outlined,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
size: 40),
const SizedBox(height: 12),
Text('Visualisation graphique en préparation',
style: AppTypography.subtitleSmall),
],
),
),
);
}),
],
),
);
@@ -400,19 +294,19 @@ class _ReportsPageState extends State<ReportsPage>
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.05),
color: AppColors.border.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)),
border: Border.all(color: AppColors.border.withOpacity(0.1)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1),
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: AppColors.primaryGreen, size: 20),
child: Icon(icon, color: AppColors.primary, size: 20),
),
const SizedBox(width: 16),
Expanded(
@@ -425,7 +319,11 @@ class _ReportsPageState extends State<ReportsPage>
],
),
),
const Icon(Icons.chevron_right_outlined, color: AppColors.textSecondaryLight, size: 20),
Icon(Icons.chevron_right_outlined,
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
size: 20),
],
),
),
@@ -436,6 +334,7 @@ class _ReportsPageState extends State<ReportsPage>
/// Onglet membres
Widget _buildMembersTab() {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
children: [
@@ -473,9 +372,9 @@ class _ReportsPageState extends State<ReportsPage>
Row(
children: [
Expanded(child: _buildStatItem('Total', total)),
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
Expanded(child: _buildStatItem('Nouveaux', nouveaux)),
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
Expanded(child: _buildStatItem('Actifs %', actifs)),
],
),
@@ -512,6 +411,7 @@ class _ReportsPageState extends State<ReportsPage>
/// Onglet organisations
Widget _buildOrganizationsTab() {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
children: [
@@ -537,7 +437,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [
Row(
children: [
const Icon(Icons.business_center_outlined, color: AppColors.primaryGreen, size: 20),
const Icon(Icons.business_center_outlined, color: AppColors.primary, size: 20),
const SizedBox(width: 12),
Text(
'Indicateurs Organisations'.toUpperCase(),
@@ -549,9 +449,9 @@ class _ReportsPageState extends State<ReportsPage>
Row(
children: [
Expanded(child: _buildStatItem('Total', total)),
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
Expanded(child: _buildStatItem('Actives', actives)),
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
Expanded(child: _buildStatItem('Membres moy.', moy)),
],
),
@@ -568,7 +468,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [
Row(
children: [
const Icon(Icons.folder_shared_outlined, color: AppColors.primaryGreen, size: 20),
const Icon(Icons.folder_shared_outlined, color: AppColors.primary, size: 20),
const SizedBox(width: 12),
Text(
'Rapports Structures'.toUpperCase(),
@@ -588,6 +488,7 @@ class _ReportsPageState extends State<ReportsPage>
/// Onglet événements
Widget _buildEventsTab() {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
children: [
@@ -625,9 +526,9 @@ class _ReportsPageState extends State<ReportsPage>
Row(
children: [
Expanded(child: _buildStatItem('Total', total)),
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
Expanded(child: _buildStatItem('À Venir', venir)),
Container(width: 1, height: 30, color: AppColors.lightBorder.withOpacity(0.2)),
Container(width: 1, height: 30, color: AppColors.border.withOpacity(0.2)),
Expanded(child: _buildStatItem('Part. moyenne', participation)),
],
),
@@ -667,7 +568,7 @@ class _ReportsPageState extends State<ReportsPage>
children: [
Text(
value,
style: AppTypography.headerSmall.copyWith(color: AppColors.primaryGreen, fontWeight: FontWeight.bold),
style: AppTypography.headerSmall.copyWith(color: AppColors.primary, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
@@ -688,19 +589,19 @@ class _ReportsPageState extends State<ReportsPage>
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.05),
color: AppColors.border.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder.withOpacity(0.1)),
border: Border.all(color: AppColors.border.withOpacity(0.1)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1),
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: AppColors.primaryGreen, size: 20),
child: Icon(icon, color: AppColors.primary, size: 20),
),
const SizedBox(width: 16),
Expanded(
@@ -713,7 +614,11 @@ class _ReportsPageState extends State<ReportsPage>
],
),
),
const Icon(Icons.file_download_outlined, color: AppColors.textSecondaryLight, size: 20),
Icon(Icons.file_download_outlined,
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
size: 20),
],
),
),
@@ -752,7 +657,7 @@ class _ReportsPageState extends State<ReportsPage>
Navigator.of(context).pop();
context.read<ReportsBloc>().add(GenerateReportRequested('export', format: _selectedFormat));
},
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white),
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primary, foregroundColor: AppColors.onPrimary),
child: const Text('Exporter'),
),
],