import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../../members/presentation/pages/members_page_wrapper.dart'; import '../../../events/presentation/pages/events_page_wrapper.dart'; import '../../../organizations/presentation/pages/organizations_page.dart'; import '../bloc/notifications_bloc.dart'; import '../../data/models/notification_model.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/widgets/core_card.dart'; import '../../../../shared/widgets/mini_avatar.dart'; import '../../../../shared/widgets/info_badge.dart'; /// Page Notifications - UnionFlow Mobile /// /// Page complète de gestion des notifications avec historique, /// préférences, filtres et actions sur les notifications. class NotificationsPage extends StatefulWidget { const NotificationsPage({super.key}); @override State createState() => _NotificationsPageState(); } class _NotificationsPageState extends State with TickerProviderStateMixin { late TabController _tabController; String _selectedFilter = 'Toutes'; bool _showOnlyUnread = false; List _liveNotifications = []; final List _filters = [ 'Toutes', 'Membres', 'Événements', 'Organisations', 'Système', ]; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); WidgetsBinding.instance.addPostFrameCallback((_) => _loadNotificationsFromBloc()); } void _loadNotificationsFromBloc() { final authState = context.read().state; if (authState is AuthAuthenticated) { context.read().add( const LoadNotifications(), ); } } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { if (state is NotificationsLoaded) { setState(() => _liveNotifications = state.notifications); } if (state is NotificationMarkedAsRead) { setState(() => _liveNotifications = state.notifications); } if (state is NotificationsError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.message), backgroundColor: Colors.red), ); } }, builder: (context, state) { return Scaffold( backgroundColor: const Color(0xFFF8F9FA), body: Column( children: [ _buildHeader(), _buildTabBar(), Expanded( child: TabBarView( controller: _tabController, children: [ _buildNotificationsTab(), _buildPreferencesTab(), ], ), ), ], ), ); }, ); } /// Header harmonisé avec le design system Widget _buildHeader() { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), 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: 16), 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'), ], ), ); } /// Onglet des notifications Widget _buildNotificationsTab() { return Column( children: [ const SizedBox(height: 16), // Filtres et options _buildFiltersSection(), // Liste des notifications Expanded( child: _buildNotificationsList(), ), ], ); } /// Section filtres Widget _buildFiltersSection() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'FILTRER PAR :', style: AppTypography.subtitleSmall.copyWith( fontWeight: FontWeight.bold, letterSpacing: 1.1, ), ), const Spacer(), Text( 'NON LUES', style: AppTypography.badgeText.copyWith(fontSize: 9), ), const SizedBox(width: 4), SizedBox( height: 24, child: Switch( value: _showOnlyUnread, onChanged: (value) => setState(() => _showOnlyUnread = value), activeColor: AppColors.primaryGreen, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _filters.map((filter) { final isSelected = _selectedFilter == filter; return Padding( padding: const EdgeInsets.only(right: 8), child: _buildFilterChip(filter, isSelected), ); }).toList(), ), ), ], ), ); } /// Chip de filtre Widget _buildFilterChip(String label, bool isSelected) { 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, borderRadius: BorderRadius.circular(4), border: Border.all( color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder, ), ), child: Text( label.toUpperCase(), style: AppTypography.badgeText.copyWith( color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight, fontSize: 9, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ); } /// Liste des notifications 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); }, ); } /// État vide Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.notifications_none_outlined, size: 40, color: AppColors.textSecondaryLight, ), const SizedBox(height: 12), Text( 'AUCUNE NOTIFICATION', style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( _showOnlyUnread ? 'Toutes vos notifications ont été lues' : 'Votre flux est à jour.', style: AppTypography.subtitleSmall, textAlign: TextAlign.center, ), ], ), ); } /// Carte de notification Widget _buildNotificationCard(Map notification) { final isRead = notification['isRead'] as bool; final type = notification['type'] as String; final color = _getNotificationColor(type); return CoreCard( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), onTap: () => _handleNotificationTap(notification), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ MiniAvatar( fallbackText: _getNotificationIconSource(type), size: 32, backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1), iconColor: isRead ? AppColors.textSecondaryLight : color, isIcon: true, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( notification['title'].toString().toUpperCase(), style: AppTypography.actionText.copyWith( fontSize: 11, color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( notification['time'], style: AppTypography.subtitleSmall.copyWith(fontSize: 10), ), ], ), const SizedBox(height: 2), Text( notification['message'], style: AppTypography.bodyTextSmall.copyWith( color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight, fontWeight: isRead ? FontWeight.normal : FontWeight.w500, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (!isRead) ...[ const SizedBox(height: 4), InfoBadge( text: 'NOUVEAU', backgroundColor: AppColors.primaryGreen.withOpacity(0.1), textColor: AppColors.primaryGreen, ), ], ], ), ), PopupMenuButton( onSelected: (action) => _handleNotificationAction(notification, action), padding: EdgeInsets.zero, constraints: const BoxConstraints(), itemBuilder: (context) => [ PopupMenuItem( value: isRead ? 'mark_unread' : 'mark_read', child: Text(isRead ? 'Marquer non lu' : 'Marquer lu', style: AppTypography.bodyTextSmall), ), PopupMenuItem( value: 'delete', child: Text('Supprimer', style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error)), ), ], child: const Icon(Icons.more_vert, size: 14, color: AppColors.textSecondaryLight), ), ], ), ); } String _getNotificationIconSource(String type) { switch (type) { case 'Membres': return 'people'; case 'Événements': return 'event'; case 'Organisations': return 'business'; case 'Système': return 'settings'; default: return 'notifications'; } } /// Onglet préférences Widget _buildPreferencesTab() { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), // Notifications push _buildPreferenceSection( 'NOTIFICATIONS PUSH', 'Recevoir des notifications sur votre appareil', Icons.notifications_active_outlined, [ _buildPreferenceItem( 'ACTIVER LES NOTIFICATIONS', 'Recevoir toutes les notifications', true, (value) => _updatePreference('push_enabled', value), ), _buildPreferenceItem( 'SONS ET VIBRATIONS', 'Alertes sonores et vibrations', true, (value) => _updatePreference('sound_enabled', value), ), ], ), const SizedBox(height: 16), // Types de notifications _buildPreferenceSection( 'Types de notifications', 'Choisir les notifications à recevoir', Icons.category, [ _buildPreferenceItem( 'Nouveaux membres', 'Adhésions et modifications de profil', true, (value) => _updatePreference('members_notifications', value), ), _buildPreferenceItem( 'Événements', 'Créations, modifications et rappels', true, (value) => _updatePreference('events_notifications', value), ), _buildPreferenceItem( 'Organisations', 'Changements dans les organisations', false, (value) => _updatePreference('organizations_notifications', value), ), _buildPreferenceItem( 'Système', 'Mises à jour et maintenance', true, (value) => _updatePreference('system_notifications', value), ), ], ), const SizedBox(height: 16), // Email _buildPreferenceSection( 'Notifications email', 'Recevoir des notifications par email', Icons.email, [ _buildPreferenceItem( 'Résumé quotidien', 'Récapitulatif des activités du jour', false, (value) => _updatePreference('daily_summary', value), ), _buildPreferenceItem( 'Résumé hebdomadaire', 'Rapport hebdomadaire des activités', true, (value) => _updatePreference('weekly_summary', value), ), _buildPreferenceItem( 'Notifications importantes', 'Alertes critiques uniquement', true, (value) => _updatePreference('important_emails', value), ), ], ), const SizedBox(height: 80), ], ), ); } /// Section de préférence Widget _buildPreferenceSection( String title, String subtitle, IconData icon, List items, ) { return CoreCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( icon, color: AppColors.primaryGreen, size: 18, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: AppTypography.actionText.copyWith(fontSize: 12), ), Text( subtitle, style: AppTypography.subtitleSmall.copyWith(fontSize: 10), ), ], ), ), ], ), const SizedBox(height: 12), ...items, ], ), ); } /// Élément de préférence Widget _buildPreferenceItem( String title, String subtitle, bool value, Function(bool) onChanged, ) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w500), ), Text( subtitle, style: AppTypography.subtitleSmall.copyWith(fontSize: 10), ), ], ), ), SizedBox( height: 24, child: Switch( value: value, onChanged: onChanged, activeColor: AppColors.primaryGreen, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), ); } // ==================== MÉTHODES DE DONNÉES ==================== /// Obtenir les notifications filtrées List> _getFilteredNotifications() { // Utiliser les données réelles du backend si disponibles List> allNotifications; if (_liveNotifications.isNotEmpty) { allNotifications = _liveNotifications.map((n) { final dateRef = n.dateEnvoi ?? n.dateEnvoiPrevue; String timeAgo = ''; if (dateRef != null) { final diff = DateTime.now().difference(dateRef); if (diff.inMinutes < 60) timeAgo = '${diff.inMinutes} min'; else if (diff.inHours < 24) timeAgo = '${diff.inHours}h'; else timeAgo = '${diff.inDays}j'; } return { 'id': n.id, 'type': n.typeAffichage, 'title': n.sujet ?? 'Notification', 'message': n.corps ?? '', 'time': timeAgo, 'isRead': n.estLue, 'actionText': null, }; }).toList(); } else { // Données de démonstration quand le backend n'a pas encore de données allNotifications = []; } var filtered = allNotifications; if (_selectedFilter != 'Toutes') { filtered = filtered.where((n) => n['type'] == _selectedFilter).toList(); } if (_showOnlyUnread) { filtered = filtered.where((n) => !(n['isRead'] as bool)).toList(); } return filtered; } /// Obtenir la couleur selon le type de notification Color _getNotificationColor(String type) { switch (type) { case 'Membres': return AppColors.primaryGreen; case 'Événements': return const Color(0xFF00B894); case 'Organisations': return AppColors.primaryGreen; case 'Système': return AppColors.warning; default: return AppColors.textSecondaryLight; } } /// Obtenir l'icône selon le type de notification IconData _getNotificationIcon(String type) { switch (type) { case 'Membres': return Icons.person_add; case 'Événements': return Icons.event; case 'Organisations': return Icons.business; case 'Système': return Icons.system_update; default: return Icons.notifications; } } // ==================== MÉTHODES D'ACTION ==================== /// Gérer le tap sur une notification void _handleNotificationTap(Map notification) { // Marquer comme lue via BLoC si non lue if (!(notification['isRead'] as bool)) { context.read().add( MarkNotificationAsRead(notification['id'].toString()), ); } // Action selon le type : navigation vers l'écran concerné final type = notification['type'] as String; switch (type) { case 'Membres': Navigator.of(context).push( MaterialPageRoute(builder: (_) => const MembersPageWrapper()), ); break; case 'Événements': Navigator.of(context).push( MaterialPageRoute(builder: (_) => const EventsPageWrapper()), ); break; case 'Organisations': Navigator.of(context).push( MaterialPageRoute(builder: (_) => const OrganizationsPage()), ); break; case 'Système': _showSystemNotificationDialog(notification); break; } } /// Gérer les actions du menu contextuel void _handleNotificationAction(Map notification, String action) { switch (action) { case 'mark_read': setState(() { notification['isRead'] = true; }); _showSuccessSnackBar('Notification marquée comme lue'); break; case 'mark_unread': setState(() { notification['isRead'] = false; }); _showSuccessSnackBar('Notification marquée comme non lue'); break; case 'delete': _showDeleteConfirmationDialog(notification); break; } } /// Marquer toutes les notifications comme lues void _markAllAsRead() { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Vider le flux', style: AppTypography.headerSmall), content: Text( 'Voulez-vous marquer toutes les notifications comme lues ?', style: AppTypography.bodyTextSmall, ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight)), ), TextButton( onPressed: () { Navigator.of(context).pop(); setState(() { final notifications = _getFilteredNotifications(); for (var notification in notifications) { notification['isRead'] = true; } }); _showSuccessSnackBar('Flux marqué comme lu'); }, child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primaryGreen)), ), ], ), ); } /// Afficher les paramètres de notification void _showNotificationSettings() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Paramètres de notification'), content: const Text( 'Utilisez l\'onglet "Préférences" pour configurer vos notifications ' 'ou accédez aux paramètres système de votre appareil.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); _tabController.animateTo(1); // Aller à l'onglet Préférences }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, ), child: const Text('Voir les préférences'), ), ], ), ); } /// Dialogue de confirmation de suppression void _showDeleteConfirmationDialog(Map 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.of(context).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); setState(() { // Simuler la suppression (dans une vraie app, on supprimerait de la base de données) }); _showSuccessSnackBar('Notification supprimée'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Supprimer'), ), ], ), ); } /// Dialogue pour les notifications système void _showSystemNotificationDialog(Map notification) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(notification['title']), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(notification['message']), const SizedBox(height: 16), if (notification['actionText'] != null) Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFE17055).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( 'Action disponible : ${notification['actionText']}', style: const TextStyle( color: Color(0xFFE17055), fontWeight: FontWeight.w600, ), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), if (notification['actionText'] != null) ElevatedButton( onPressed: () { Navigator.of(context).pop(); _showSuccessSnackBar('Action "${notification['actionText']}" exécutée'); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE17055), foregroundColor: Colors.white, ), child: Text(notification['actionText']), ), ], ), ); } /// Mettre à jour une préférence void _updatePreference(String key, bool value) { // Ici on sauvegarderait dans les préférences locales ou sur le serveur _showSuccessSnackBar( value ? 'Préférence activée' : 'Préférence désactivée' ); } /// Afficher un message de succès void _showSuccessSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: const Color(0xFF00B894), behavior: SnackBarBehavior.floating, ), ); } /// Afficher un message d'erreur void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: const Color(0xFFE74C3C), behavior: SnackBarBehavior.floating, ), ); } }