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:
@@ -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'),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user