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

@@ -71,126 +71,54 @@ class _NotificationsPageState extends State<NotificationsPage>
}
if (state is NotificationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
);
}
},
builder: (context, state) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
body: Column(
children: [
_buildHeader(),
_buildTabBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildNotificationsTab(),
_buildPreferencesTab(),
],
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Notifications',
moduleGradient: ModuleColors.notificationsGradient,
actions: [
IconButton(
onPressed: () => _markAllAsRead(),
icon: const Icon(Icons.done_all, size: 20),
tooltip: 'Tout marquer comme lu',
),
],
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('FLUX')),
Tab(child: Text('RÉGLAGES')),
],
),
),
body: SafeArea(
top: false,
child: TabBarView(
controller: _tabController,
children: [
_buildNotificationsTab(),
_buildPreferencesTab(),
],
),
),
);
},
);
}
/// Header harmonisé avec le design system
Widget _buildHeader() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.brandGreen, AppColors.primaryGreen],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x1A000000),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.notifications_none,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'NOTIFICATIONS',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.1,
),
),
Text(
'Restez connecté à votre réseau',
style: TextStyle(
fontSize: 11,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
IconButton(
onPressed: () => _markAllAsRead(),
icon: const Icon(Icons.done_all, color: Colors.white, size: 20),
tooltip: 'Tout marquer comme lu',
),
],
),
);
}
/// Barre d'onglets
Widget _buildTabBar() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(8),
),
child: TabBar(
controller: _tabController,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColors.primaryGreen.withOpacity(0.1),
),
labelStyle: AppTypography.actionText.copyWith(fontSize: 12),
unselectedLabelStyle: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
tabs: const [
Tab(text: 'FLUX'),
Tab(text: 'RÉGLAGES'),
],
),
);
}
// _buildHeader() et _buildTabBar() supprimés : gradient + TabBar migrés
// dans UFAppBar + UFAppBar.bottom (pattern Adhésions).
/// Onglet des notifications
Widget _buildNotificationsTab() {
@@ -236,7 +164,7 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Switch(
value: _showOnlyUnread,
onChanged: (value) => setState(() => _showOnlyUnread = value),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
@@ -262,22 +190,25 @@ class _NotificationsPageState extends State<NotificationsPage>
/// Chip de filtre
Widget _buildFilterChip(String label, bool isSelected) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final borderInactive= isDark ? AppColors.borderDark : AppColors.border;
final textInactive = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return InkWell(
onTap: () => setState(() => _selectedFilter = label),
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryGreen.withOpacity(0.1) : Colors.transparent,
color: isSelected ? AppColors.primary.withOpacity(isDark ? 0.2 : 0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
color: isSelected ? AppColors.primary : borderInactive,
),
),
child: Text(
label.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
color: isSelected ? AppColors.primary : textInactive,
fontSize: 9,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
@@ -290,17 +221,24 @@ class _NotificationsPageState extends State<NotificationsPage>
Widget _buildNotificationsList() {
final notifications = _getFilteredNotifications();
if (notifications.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: notifications.length,
itemBuilder: (context, index) {
final notification = notifications[index];
return _buildNotificationCard(notification);
},
return RefreshIndicator(
color: ModuleColors.notifications,
onRefresh: () async =>
context.read<NotificationsBloc>().add(const LoadNotifications()),
child: notifications.isEmpty
? SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: _buildEmptyState(),
)
: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
itemCount: notifications.length,
itemBuilder: (context, index) {
final notification = notifications[index];
return _buildNotificationCard(notification);
},
),
);
}
@@ -310,11 +248,11 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icon(
Icons.notifications_none_outlined,
size: 40,
color: AppColors.textSecondaryLight,
),
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 12),
Text(
'AUCUNE NOTIFICATION',
@@ -338,6 +276,10 @@ class _NotificationsPageState extends State<NotificationsPage>
final isRead = notification['isRead'] as bool;
final type = notification['type'] as String;
final color = _getNotificationColor(type);
final isDark = Theme.of(context).brightness == Brightness.dark;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
final bgRead = isDark ? AppColors.surfaceVariantDark : AppColors.surface;
return CoreCard(
margin: const EdgeInsets.only(bottom: 8),
@@ -349,8 +291,8 @@ class _NotificationsPageState extends State<NotificationsPage>
MiniAvatar(
fallbackText: _getNotificationIconSource(type),
size: 32,
backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1),
iconColor: isRead ? AppColors.textSecondaryLight : color,
backgroundColor: isRead ? bgRead : color.withOpacity(isDark ? 0.2 : 0.1),
iconColor: isRead ? textSecondary : color,
isIcon: true,
),
const SizedBox(width: 12),
@@ -366,7 +308,7 @@ class _NotificationsPageState extends State<NotificationsPage>
notification['title'].toString().toUpperCase(),
style: AppTypography.actionText.copyWith(
fontSize: 11,
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
color: isRead ? textSecondary : textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -382,7 +324,7 @@ class _NotificationsPageState extends State<NotificationsPage>
Text(
notification['message'],
style: AppTypography.bodyTextSmall.copyWith(
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
color: isRead ? textSecondary : textPrimary,
fontWeight: isRead ? FontWeight.normal : FontWeight.w500,
),
maxLines: 2,
@@ -392,8 +334,8 @@ class _NotificationsPageState extends State<NotificationsPage>
const SizedBox(height: 4),
InfoBadge(
text: 'NOUVEAU',
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
textColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary.withOpacity(0.1),
textColor: AppColors.primary,
),
],
],
@@ -413,7 +355,7 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Text('Supprimer', style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error)),
),
],
child: const Icon(Icons.more_vert, size: 14, color: AppColors.textSecondaryLight),
child: Icon(Icons.more_vert, size: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
@@ -545,7 +487,7 @@ class _NotificationsPageState extends State<NotificationsPage>
children: [
Icon(
icon,
color: AppColors.primaryGreen,
color: AppColors.primary,
size: 18,
),
const SizedBox(width: 10),
@@ -604,7 +546,7 @@ class _NotificationsPageState extends State<NotificationsPage>
child: Switch(
value: value,
onChanged: onChanged,
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
@@ -659,15 +601,15 @@ class _NotificationsPageState extends State<NotificationsPage>
Color _getNotificationColor(String type) {
switch (type) {
case 'Membres':
return AppColors.primaryGreen;
return AppColors.primary;
case 'Événements':
return AppColors.success;
case 'Organisations':
return AppColors.primaryGreen;
return AppColors.primary;
case 'Système':
return AppColors.warning;
default:
return AppColors.textSecondaryLight;
return AppColors.textSecondary;
}
}
@@ -756,7 +698,7 @@ class _NotificationsPageState extends State<NotificationsPage>
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight)),
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
TextButton(
onPressed: () {
@@ -769,7 +711,7 @@ class _NotificationsPageState extends State<NotificationsPage>
});
_showSuccessSnackBar('Flux marqué comme lu');
},
child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primaryGreen)),
child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primary)),
),
],
),
@@ -797,8 +739,8 @@ class _NotificationsPageState extends State<NotificationsPage>
_tabController.animateTo(1); // Aller à l'onglet Préférences
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Voir les préférences'),
),
@@ -831,8 +773,8 @@ class _NotificationsPageState extends State<NotificationsPage>
_showSuccessSnackBar('Notification supprimée');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: AppColors.error,
foregroundColor: AppColors.onError,
),
child: const Text('Supprimer'),
),
@@ -884,7 +826,7 @@ class _NotificationsPageState extends State<NotificationsPage>
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.warning,
foregroundColor: Colors.white,
foregroundColor: AppColors.onPrimary,
),
child: Text(notification['actionText']),
),