import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/widgets/unified_page_layout.dart'; import '../../../../core/widgets/unified_card.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_text_styles.dart'; import '../../../../core/utils/date_formatter.dart'; import '../../domain/entities/notification.dart'; import '../bloc/notifications_bloc.dart'; import '../widgets/notification_card_widget.dart'; import '../widgets/notification_filter_widget.dart'; import '../widgets/notification_search_widget.dart'; import '../widgets/notification_stats_widget.dart'; /// Page principale du centre de notifications class NotificationsCenterPage extends StatefulWidget { const NotificationsCenterPage({super.key}); @override State createState() => _NotificationsCenterPageState(); } class _NotificationsCenterPageState extends State with TickerProviderStateMixin { late TabController _tabController; final ScrollController _scrollController = ScrollController(); bool _showSearch = false; String _searchQuery = ''; Set _selectedTypes = {}; Set _selectedStatuts = {}; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); _scrollController.addListener(_onScroll); // Chargement initial des notifications context.read().add( const LoadNotificationsEvent(forceRefresh: false), ); } @override void dispose() { _tabController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { // Chargement de plus de notifications (pagination) context.read().add( const LoadMoreNotificationsEvent(), ); } } void _onRefresh() { context.read().add( const LoadNotificationsEvent(forceRefresh: true), ); } void _toggleSearch() { setState(() { _showSearch = !_showSearch; if (!_showSearch) { _searchQuery = ''; _applyFilters(); } }); } void _onSearchChanged(String query) { setState(() { _searchQuery = query; }); _applyFilters(); } void _onFiltersChanged({ Set? types, Set? statuts, }) { setState(() { if (types != null) _selectedTypes = types; if (statuts != null) _selectedStatuts = statuts; }); _applyFilters(); } void _applyFilters() { context.read().add( SearchNotificationsEvent( query: _searchQuery.isEmpty ? null : _searchQuery, types: _selectedTypes.isEmpty ? null : _selectedTypes.toList(), statuts: _selectedStatuts.isEmpty ? null : _selectedStatuts.toList(), ), ); } void _markAllAsRead() { context.read().add( const MarkAllAsReadEvent(), ); } void _archiveAllRead() { context.read().add( const ArchiveAllReadEvent(), ); } @override Widget build(BuildContext context) { return UnifiedPageLayout( title: 'Notifications', showBackButton: true, actions: [ IconButton( icon: Icon(_showSearch ? Icons.search_off : Icons.search), onPressed: _toggleSearch, tooltip: _showSearch ? 'Fermer la recherche' : 'Rechercher', ), PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (value) { switch (value) { case 'mark_all_read': _markAllAsRead(); break; case 'archive_all_read': _archiveAllRead(); break; case 'preferences': Navigator.pushNamed(context, '/notifications/preferences'); break; case 'export': Navigator.pushNamed(context, '/notifications/export'); break; } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'mark_all_read', child: ListTile( leading: Icon(Icons.mark_email_read), title: Text('Tout marquer comme lu'), contentPadding: EdgeInsets.zero, ), ), const PopupMenuItem( value: 'archive_all_read', child: ListTile( leading: Icon(Icons.archive), title: Text('Archiver tout lu'), contentPadding: EdgeInsets.zero, ), ), const PopupMenuDivider(), const PopupMenuItem( value: 'preferences', child: ListTile( leading: Icon(Icons.settings), title: Text('Préférences'), contentPadding: EdgeInsets.zero, ), ), const PopupMenuItem( value: 'export', child: ListTile( leading: Icon(Icons.download), title: Text('Exporter'), contentPadding: EdgeInsets.zero, ), ), ], ), ], body: Column( children: [ // Barre de recherche (conditionnelle) if (_showSearch) Padding( padding: const EdgeInsets.all(16.0), child: NotificationSearchWidget( onSearchChanged: _onSearchChanged, onFiltersChanged: _onFiltersChanged, selectedTypes: _selectedTypes, selectedStatuts: _selectedStatuts, ), ), // Statistiques rapides BlocBuilder( builder: (context, state) { if (state is NotificationsLoaded) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: NotificationStatsWidget( totalCount: state.notifications.length, unreadCount: state.unreadCount, importantCount: state.notifications .where((n) => n.estImportante) .length, ), ); } return const SizedBox.shrink(); }, ), const SizedBox(height: 16), // Onglets de filtrage Container( margin: const EdgeInsets.symmetric(horizontal: 16.0), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.outline.withOpacity(0.2)), ), child: TabBar( controller: _tabController, indicator: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(8), ), indicatorSize: TabBarIndicatorSize.tab, indicatorPadding: const EdgeInsets.all(4), labelColor: AppColors.onPrimary, unselectedLabelColor: AppColors.onSurface, labelStyle: AppTextStyles.bodyMedium.copyWith( fontWeight: FontWeight.w600, ), unselectedLabelStyle: AppTextStyles.bodyMedium, tabs: const [ Tab(text: 'Toutes'), Tab(text: 'Non lues'), Tab(text: 'Importantes'), Tab(text: 'Archivées'), ], ), ), const SizedBox(height: 16), // Liste des notifications Expanded( child: TabBarView( controller: _tabController, children: [ _buildNotificationsList(NotificationFilter.all), _buildNotificationsList(NotificationFilter.unread), _buildNotificationsList(NotificationFilter.important), _buildNotificationsList(NotificationFilter.archived), ], ), ), ], ), ); } Widget _buildNotificationsList(NotificationFilter filter) { return BlocBuilder( builder: (context, state) { if (state is NotificationsLoading && state.notifications.isEmpty) { return const Center( child: CircularProgressIndicator(), ); } if (state is NotificationsError && state.notifications.isEmpty) { return Center( child: UnifiedCard( variant: UnifiedCardVariant.outlined, child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 48, color: AppColors.error, ), const SizedBox(height: 16), Text( 'Erreur de chargement', style: AppTextStyles.titleMedium, ), const SizedBox(height: 8), Text( state.message, style: AppTextStyles.bodyMedium.copyWith( color: AppColors.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: _onRefresh, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), ), ], ), ), ), ); } final notifications = _filterNotifications( state.notifications, filter, ); if (notifications.isEmpty) { return Center( child: UnifiedCard( variant: UnifiedCardVariant.outlined, child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( _getEmptyIcon(filter), size: 48, color: AppColors.onSurface.withOpacity(0.5), ), const SizedBox(height: 16), Text( _getEmptyTitle(filter), style: AppTextStyles.titleMedium, ), const SizedBox(height: 8), Text( _getEmptyMessage(filter), style: AppTextStyles.bodyMedium.copyWith( color: AppColors.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), ], ), ), ), ); } return RefreshIndicator( onRefresh: () async => _onRefresh(), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16.0), itemCount: notifications.length + (state.hasMore ? 1 : 0), itemBuilder: (context, index) { if (index >= notifications.length) { // Indicateur de chargement pour la pagination return const Padding( padding: EdgeInsets.all(16.0), child: Center( child: CircularProgressIndicator(), ), ); } final notification = notifications[index]; return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: NotificationCardWidget( notification: notification, onTap: () => _onNotificationTap(notification), onMarkAsRead: () => _onMarkAsRead(notification), onMarkAsImportant: () => _onMarkAsImportant(notification), onArchive: () => _onArchive(notification), onDelete: () => _onDelete(notification), onActionTap: (action) => _onActionTap(notification, action), ), ); }, ), ); }, ); } List _filterNotifications( List notifications, NotificationFilter filter, ) { switch (filter) { case NotificationFilter.all: return notifications.where((n) => !n.estArchivee).toList(); case NotificationFilter.unread: return notifications.where((n) => !n.estLue && !n.estArchivee).toList(); case NotificationFilter.important: return notifications.where((n) => n.estImportante && !n.estArchivee).toList(); case NotificationFilter.archived: return notifications.where((n) => n.estArchivee).toList(); } } IconData _getEmptyIcon(NotificationFilter filter) { switch (filter) { case NotificationFilter.all: return Icons.notifications_none; case NotificationFilter.unread: return Icons.mark_email_read; case NotificationFilter.important: return Icons.star_border; case NotificationFilter.archived: return Icons.archive; } } String _getEmptyTitle(NotificationFilter filter) { switch (filter) { case NotificationFilter.all: return 'Aucune notification'; case NotificationFilter.unread: return 'Tout est lu !'; case NotificationFilter.important: return 'Aucune notification importante'; case NotificationFilter.archived: return 'Aucune notification archivée'; } } String _getEmptyMessage(NotificationFilter filter) { switch (filter) { case NotificationFilter.all: return 'Vous n\'avez encore reçu aucune notification.'; case NotificationFilter.unread: return 'Toutes vos notifications ont été lues.'; case NotificationFilter.important: return 'Vous n\'avez aucune notification marquée comme importante.'; case NotificationFilter.archived: return 'Vous n\'avez aucune notification archivée.'; } } void _onNotificationTap(NotificationEntity notification) { // Marquer comme lue si pas encore lue if (!notification.estLue) { _onMarkAsRead(notification); } // Navigation vers le détail ou action par défaut if (notification.actionClic != null) { Navigator.pushNamed( context, notification.actionClic!, arguments: notification.parametresAction, ); } else { Navigator.pushNamed( context, '/notifications/detail', arguments: notification.id, ); } } void _onMarkAsRead(NotificationEntity notification) { context.read().add( MarkAsReadEvent(notificationId: notification.id), ); } void _onMarkAsImportant(NotificationEntity notification) { context.read().add( MarkAsImportantEvent( notificationId: notification.id, important: !notification.estImportante, ), ); } void _onArchive(NotificationEntity notification) { context.read().add( ArchiveNotificationEvent(notificationId: notification.id), ); } void _onDelete(NotificationEntity notification) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Supprimer la notification'), content: const Text( 'Êtes-vous sûr de vouloir supprimer cette notification ? ' 'Cette action est irréversible.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), TextButton( onPressed: () { Navigator.pop(context); context.read().add( DeleteNotificationEvent(notificationId: notification.id), ); }, style: TextButton.styleFrom( foregroundColor: AppColors.error, ), child: const Text('Supprimer'), ), ], ), ); } void _onActionTap(NotificationEntity notification, ActionNotification action) { context.read().add( ExecuteQuickActionEvent( notificationId: notification.id, actionId: action.id, parameters: action.parametres, ), ); } } /// Énumération des filtres de notification enum NotificationFilter { all, unread, important, archived, }