Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -1,779 +0,0 @@
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 '../../domain/entities/notification.dart';
import '../../domain/entities/preferences_notification.dart';
import '../bloc/notification_preferences_bloc.dart';
import '../widgets/preference_section_widget.dart';
import '../widgets/silent_mode_config_widget.dart';
/// Page de configuration des préférences de notifications
class NotificationPreferencesPage extends StatefulWidget {
const NotificationPreferencesPage({super.key});
@override
State<NotificationPreferencesPage> createState() => _NotificationPreferencesPageState();
}
class _NotificationPreferencesPageState extends State<NotificationPreferencesPage>
with TickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// Chargement des préférences
context.read<NotificationPreferencesBloc>().add(
const LoadPreferencesEvent(),
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return UnifiedPageLayout(
title: 'Préférences de notifications',
showBackButton: true,
actions: [
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
switch (value) {
case 'reset':
_showResetDialog();
break;
case 'test':
_sendTestNotification();
break;
case 'export':
_exportPreferences();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'test',
child: ListTile(
leading: Icon(Icons.send),
title: Text('Envoyer un test'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuItem(
value: 'export',
child: ListTile(
leading: Icon(Icons.download),
title: Text('Exporter'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'reset',
child: ListTile(
leading: Icon(Icons.restore, color: Colors.red),
title: Text('Réinitialiser', style: TextStyle(color: Colors.red)),
contentPadding: EdgeInsets.zero,
),
),
],
),
],
body: BlocBuilder<NotificationPreferencesBloc, NotificationPreferencesState>(
builder: (context, state) {
if (state is PreferencesLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state is PreferencesError) {
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: () {
context.read<NotificationPreferencesBloc>().add(
const LoadPreferencesEvent(),
);
},
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
),
),
);
}
if (state is! PreferencesLoaded) {
return const SizedBox.shrink();
}
return Column(
children: [
// Onglets de navigation
Container(
margin: const EdgeInsets.all(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: 'Général'),
Tab(text: 'Types'),
Tab(text: 'Avancé'),
],
),
),
// Contenu des onglets
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildGeneralTab(state.preferences),
_buildTypesTab(state.preferences),
_buildAdvancedTab(state.preferences),
],
),
),
],
);
},
),
);
}
Widget _buildGeneralTab(PreferencesNotificationEntity preferences) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Activation générale
PreferenceSectionWidget(
title: 'Notifications',
subtitle: 'Paramètres généraux des notifications',
icon: Icons.notifications,
children: [
SwitchListTile(
title: const Text('Activer les notifications'),
subtitle: const Text('Recevoir toutes les notifications'),
value: preferences.notificationsActivees,
onChanged: (value) => _updatePreference(
preferences.copyWith(notificationsActivees: value),
),
),
if (preferences.notificationsActivees) ...[
SwitchListTile(
title: const Text('Notifications push'),
subtitle: const Text('Recevoir les notifications sur l\'appareil'),
value: preferences.pushActivees,
onChanged: (value) => _updatePreference(
preferences.copyWith(pushActivees: value),
),
),
SwitchListTile(
title: const Text('Notifications par email'),
subtitle: const Text('Recevoir les notifications par email'),
value: preferences.emailActivees,
onChanged: (value) => _updatePreference(
preferences.copyWith(emailActivees: value),
),
),
SwitchListTile(
title: const Text('Notifications SMS'),
subtitle: const Text('Recevoir les notifications par SMS'),
value: preferences.smsActivees,
onChanged: (value) => _updatePreference(
preferences.copyWith(smsActivees: value),
),
),
],
],
),
const SizedBox(height: 24),
// Mode silencieux
PreferenceSectionWidget(
title: 'Mode silencieux',
subtitle: 'Configurer les périodes de silence',
icon: Icons.do_not_disturb,
children: [
SilentModeConfigWidget(
preferences: preferences,
onPreferencesChanged: _updatePreference,
),
],
),
const SizedBox(height: 24),
// Paramètres visuels et sonores
PreferenceSectionWidget(
title: 'Apparence et sons',
subtitle: 'Personnaliser l\'affichage des notifications',
icon: Icons.palette,
children: [
SwitchListTile(
title: const Text('Vibration'),
subtitle: const Text('Faire vibrer l\'appareil'),
value: preferences.vibrationActivee,
onChanged: (value) => _updatePreference(
preferences.copyWith(vibrationActivee: value),
),
),
SwitchListTile(
title: const Text('Son'),
subtitle: const Text('Jouer un son'),
value: preferences.sonActive,
onChanged: (value) => _updatePreference(
preferences.copyWith(sonActive: value),
),
),
SwitchListTile(
title: const Text('LED'),
subtitle: const Text('Allumer la LED de notification'),
value: preferences.ledActivee,
onChanged: (value) => _updatePreference(
preferences.copyWith(ledActivee: value),
),
),
SwitchListTile(
title: const Text('Aperçu sur écran verrouillé'),
subtitle: const Text('Afficher le contenu sur l\'écran verrouillé'),
value: preferences.apercuEcranVerrouillage,
onChanged: (value) => _updatePreference(
preferences.copyWith(apercuEcranVerrouillage: value),
),
),
],
),
],
),
);
}
Widget _buildTypesTab(PreferencesNotificationEntity preferences) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Choisissez les types de notifications que vous souhaitez recevoir',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
const SizedBox(height: 16),
// Groupement par catégorie
..._buildTypesByCategory(preferences),
],
),
);
}
Widget _buildAdvancedTab(PreferencesNotificationEntity preferences) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gestion automatique
PreferenceSectionWidget(
title: 'Gestion automatique',
subtitle: 'Paramètres de gestion automatique des notifications',
icon: Icons.auto_mode,
children: [
SwitchListTile(
title: const Text('Marquage automatique comme lu'),
subtitle: const Text('Marquer automatiquement les notifications comme lues'),
value: preferences.marquageLectureAutomatique,
onChanged: (value) => _updatePreference(
preferences.copyWith(marquageLectureAutomatique: value),
),
),
if (preferences.marquageLectureAutomatique)
ListTile(
title: const Text('Délai de marquage'),
subtitle: Text('${preferences.delaiMarquageLectureSecondes ?? 5} secondes'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showDelayPicker(
'Délai de marquage automatique',
preferences.delaiMarquageLectureSecondes ?? 5,
(value) => _updatePreference(
preferences.copyWith(delaiMarquageLectureSecondes: value),
),
),
),
SwitchListTile(
title: const Text('Archivage automatique'),
subtitle: const Text('Archiver automatiquement les notifications lues'),
value: preferences.archivageAutomatique,
onChanged: (value) => _updatePreference(
preferences.copyWith(archivageAutomatique: value),
),
),
if (preferences.archivageAutomatique)
ListTile(
title: const Text('Délai d\'archivage'),
subtitle: Text('${preferences.delaiArchivageHeures} heures'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showDelayPicker(
'Délai d\'archivage automatique',
preferences.delaiArchivageHeures,
(value) => _updatePreference(
preferences.copyWith(delaiArchivageHeures: value),
),
),
),
],
),
const SizedBox(height: 24),
// Limites et regroupement
PreferenceSectionWidget(
title: 'Limites et regroupement',
subtitle: 'Contrôler le nombre et le regroupement des notifications',
icon: Icons.group_work,
children: [
ListTile(
title: const Text('Notifications simultanées maximum'),
subtitle: Text('${preferences.maxNotificationsSimultanees} notifications'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showNumberPicker(
'Nombre maximum de notifications simultanées',
preferences.maxNotificationsSimultanees,
1,
50,
(value) => _updatePreference(
preferences.copyWith(maxNotificationsSimultanees: value),
),
),
),
ListTile(
title: const Text('Fréquence de regroupement'),
subtitle: Text('${preferences.frequenceRegroupementMinutes} minutes'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showNumberPicker(
'Fréquence de regroupement des notifications',
preferences.frequenceRegroupementMinutes,
1,
60,
(value) => _updatePreference(
preferences.copyWith(frequenceRegroupementMinutes: value),
),
),
),
ListTile(
title: const Text('Durée d\'affichage'),
subtitle: Text('${preferences.dureeAffichageSecondes} secondes'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showNumberPicker(
'Durée d\'affichage des notifications',
preferences.dureeAffichageSecondes,
3,
30,
(value) => _updatePreference(
preferences.copyWith(dureeAffichageSecondes: value),
),
),
),
],
),
const SizedBox(height: 24),
// Conservation des données
PreferenceSectionWidget(
title: 'Conservation des données',
subtitle: 'Durée de conservation des notifications',
icon: Icons.storage,
children: [
ListTile(
title: const Text('Durée de conservation'),
subtitle: Text('${preferences.dureeConservationJours} jours'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showNumberPicker(
'Durée de conservation des notifications',
preferences.dureeConservationJours,
7,
365,
(value) => _updatePreference(
preferences.copyWith(dureeConservationJours: value),
),
),
),
SwitchListTile(
title: const Text('Affichage de l\'historique'),
subtitle: const Text('Conserver l\'historique des notifications'),
value: preferences.affichageHistorique,
onChanged: (value) => _updatePreference(
preferences.copyWith(affichageHistorique: value),
),
),
],
),
],
),
);
}
List<Widget> _buildTypesByCategory(PreferencesNotificationEntity preferences) {
final typesByCategory = <String, List<TypeNotification>>{};
for (final type in TypeNotification.values) {
typesByCategory.putIfAbsent(type.categorie, () => []).add(type);
}
return typesByCategory.entries.map((entry) {
return PreferenceSectionWidget(
title: _getCategoryTitle(entry.key),
subtitle: _getCategorySubtitle(entry.key),
icon: _getCategoryIcon(entry.key),
children: entry.value.map((type) {
return SwitchListTile(
title: Text(type.libelle),
subtitle: Text(_getTypeDescription(type)),
value: preferences.isTypeActive(type),
onChanged: (value) => _toggleNotificationType(type, value),
secondary: Icon(
_getTypeIconData(type),
color: _getTypeColor(type),
),
);
}).toList(),
);
}).toList();
}
String _getCategoryTitle(String category) {
switch (category) {
case 'evenements':
return 'Événements';
case 'cotisations':
return 'Cotisations';
case 'solidarite':
return 'Solidarité';
case 'membres':
return 'Membres';
case 'organisation':
return 'Organisation';
case 'messages':
return 'Messages';
case 'systeme':
return 'Système';
default:
return category;
}
}
String _getCategorySubtitle(String category) {
switch (category) {
case 'evenements':
return 'Notifications liées aux événements';
case 'cotisations':
return 'Notifications de paiement et cotisations';
case 'solidarite':
return 'Demandes d\'aide et solidarité';
case 'membres':
return 'Nouveaux membres et anniversaires';
case 'organisation':
return 'Annonces et réunions';
case 'messages':
return 'Messages privés et mentions';
case 'systeme':
return 'Mises à jour et maintenance';
default:
return '';
}
}
IconData _getCategoryIcon(String category) {
switch (category) {
case 'evenements':
return Icons.event;
case 'cotisations':
return Icons.payment;
case 'solidarite':
return Icons.volunteer_activism;
case 'membres':
return Icons.people;
case 'organisation':
return Icons.business;
case 'messages':
return Icons.message;
case 'systeme':
return Icons.settings;
default:
return Icons.notifications;
}
}
String _getTypeDescription(TypeNotification type) {
// Descriptions courtes pour chaque type
switch (type) {
case TypeNotification.nouvelEvenement:
return 'Nouveaux événements créés';
case TypeNotification.rappelEvenement:
return 'Rappels avant les événements';
case TypeNotification.cotisationDue:
return 'Échéances de cotisations';
case TypeNotification.cotisationPayee:
return 'Confirmations de paiement';
case TypeNotification.nouvelleDemandeAide:
return 'Nouvelles demandes d\'aide';
case TypeNotification.nouveauMembre:
return 'Nouveaux membres rejoignant';
case TypeNotification.anniversaireMembre:
return 'Anniversaires des membres';
case TypeNotification.annonceGenerale:
return 'Annonces importantes';
case TypeNotification.messagePrive:
return 'Messages privés reçus';
default:
return type.libelle;
}
}
IconData _getTypeIconData(TypeNotification type) {
switch (type.icone) {
case 'event':
return Icons.event;
case 'payment':
return Icons.payment;
case 'help':
return Icons.help;
case 'person_add':
return Icons.person_add;
case 'cake':
return Icons.cake;
case 'campaign':
return Icons.campaign;
case 'mail':
return Icons.mail;
default:
return Icons.notifications;
}
}
Color _getTypeColor(TypeNotification type) {
try {
return Color(int.parse(type.couleur.replaceFirst('#', '0xFF')));
} catch (e) {
return AppColors.primary;
}
}
void _updatePreference(PreferencesNotificationEntity preferences) {
context.read<NotificationPreferencesBloc>().add(
UpdatePreferencesEvent(preferences: preferences),
);
}
void _toggleNotificationType(TypeNotification type, bool active) {
context.read<NotificationPreferencesBloc>().add(
ToggleNotificationTypeEvent(type: type, active: active),
);
}
void _showResetDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Réinitialiser les préférences'),
content: const Text(
'Êtes-vous sûr de vouloir réinitialiser toutes vos préférences '
'de notifications aux valeurs par défaut ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.read<NotificationPreferencesBloc>().add(
const ResetPreferencesEvent(),
);
},
style: TextButton.styleFrom(
foregroundColor: AppColors.error,
),
child: const Text('Réinitialiser'),
),
],
),
);
}
void _sendTestNotification() {
context.read<NotificationPreferencesBloc>().add(
const SendTestNotificationEvent(type: TypeNotification.annonceGenerale),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notification de test envoyée'),
duration: Duration(seconds: 2),
),
);
}
void _exportPreferences() {
context.read<NotificationPreferencesBloc>().add(
const ExportPreferencesEvent(),
);
}
void _showDelayPicker(String title, int currentValue, Function(int) onChanged) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Valeur actuelle: $currentValue secondes'),
const SizedBox(height: 16),
// Ici vous pourriez ajouter un slider ou un picker
// Pour simplifier, on utilise des boutons prédéfinis
Wrap(
spacing: 8,
children: [5, 10, 15, 30, 60].map((value) {
return ChoiceChip(
label: Text('${value}s'),
selected: currentValue == value,
onSelected: (selected) {
if (selected) {
onChanged(value);
Navigator.pop(context);
}
},
);
}).toList(),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
],
),
);
}
void _showNumberPicker(
String title,
int currentValue,
int min,
int max,
Function(int) onChanged,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Valeur actuelle: $currentValue'),
const SizedBox(height: 16),
// Slider pour choisir la valeur
Slider(
value: currentValue.toDouble(),
min: min.toDouble(),
max: max.toDouble(),
divisions: max - min,
label: currentValue.toString(),
onChanged: (value) {
// Mise à jour en temps réel
},
onChangeEnd: (value) {
onChanged(value.round());
Navigator.pop(context);
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
],
),
);
}
}

View File

@@ -1,539 +0,0 @@
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<NotificationsCenterPage> createState() => _NotificationsCenterPageState();
}
class _NotificationsCenterPageState extends State<NotificationsCenterPage>
with TickerProviderStateMixin {
late TabController _tabController;
final ScrollController _scrollController = ScrollController();
bool _showSearch = false;
String _searchQuery = '';
Set<TypeNotification> _selectedTypes = {};
Set<StatutNotification> _selectedStatuts = {};
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
_scrollController.addListener(_onScroll);
// Chargement initial des notifications
context.read<NotificationsBloc>().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<NotificationsBloc>().add(
const LoadMoreNotificationsEvent(),
);
}
}
void _onRefresh() {
context.read<NotificationsBloc>().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<TypeNotification>? types,
Set<StatutNotification>? statuts,
}) {
setState(() {
if (types != null) _selectedTypes = types;
if (statuts != null) _selectedStatuts = statuts;
});
_applyFilters();
}
void _applyFilters() {
context.read<NotificationsBloc>().add(
SearchNotificationsEvent(
query: _searchQuery.isEmpty ? null : _searchQuery,
types: _selectedTypes.isEmpty ? null : _selectedTypes.toList(),
statuts: _selectedStatuts.isEmpty ? null : _selectedStatuts.toList(),
),
);
}
void _markAllAsRead() {
context.read<NotificationsBloc>().add(
const MarkAllAsReadEvent(),
);
}
void _archiveAllRead() {
context.read<NotificationsBloc>().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<String>(
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<NotificationsBloc, NotificationsState>(
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<NotificationsBloc, NotificationsState>(
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<NotificationEntity> _filterNotifications(
List<NotificationEntity> 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<NotificationsBloc>().add(
MarkAsReadEvent(notificationId: notification.id),
);
}
void _onMarkAsImportant(NotificationEntity notification) {
context.read<NotificationsBloc>().add(
MarkAsImportantEvent(
notificationId: notification.id,
important: !notification.estImportante,
),
);
}
void _onArchive(NotificationEntity notification) {
context.read<NotificationsBloc>().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<NotificationsBloc>().add(
DeleteNotificationEvent(notificationId: notification.id),
);
},
style: TextButton.styleFrom(
foregroundColor: AppColors.error,
),
child: const Text('Supprimer'),
),
],
),
);
}
void _onActionTap(NotificationEntity notification, ActionNotification action) {
context.read<NotificationsBloc>().add(
ExecuteQuickActionEvent(
notificationId: notification.id,
actionId: action.id,
parameters: action.parametres,
),
);
}
}
/// Énumération des filtres de notification
enum NotificationFilter {
all,
unread,
important,
archived,
}

View File

@@ -1,430 +0,0 @@
import 'package:flutter/material.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';
/// Widget de carte pour afficher une notification
class NotificationCardWidget extends StatelessWidget {
final NotificationEntity notification;
final VoidCallback? onTap;
final VoidCallback? onMarkAsRead;
final VoidCallback? onMarkAsImportant;
final VoidCallback? onArchive;
final VoidCallback? onDelete;
final Function(ActionNotification)? onActionTap;
const NotificationCardWidget({
super.key,
required this.notification,
this.onTap,
this.onMarkAsRead,
this.onMarkAsImportant,
this.onArchive,
this.onDelete,
this.onActionTap,
});
@override
Widget build(BuildContext context) {
final isUnread = !notification.estLue;
final isImportant = notification.estImportante;
final isExpired = notification.isExpiree;
return UnifiedCard(
variant: isUnread ? UnifiedCardVariant.elevated : UnifiedCardVariant.outlined,
onTap: onTap,
child: Container(
decoration: BoxDecoration(
border: isUnread
? Border.left(
color: _getTypeColor(),
width: 4,
)
: null,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec icône, titre et actions
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icône du type de notification
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _getTypeColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getTypeIcon(),
color: _getTypeColor(),
size: 20,
),
),
const SizedBox(width: 12),
// Titre et métadonnées
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
notification.titre,
style: AppTextStyles.titleSmall.copyWith(
fontWeight: isUnread ? FontWeight.w600 : FontWeight.w500,
color: isExpired
? AppColors.onSurface.withOpacity(0.6)
: AppColors.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Badges de statut
if (isImportant) ...[
const SizedBox(width: 8),
Icon(
Icons.star,
color: AppColors.warning,
size: 16,
),
],
if (isUnread) ...[
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColors.primary,
shape: BoxShape.circle,
),
),
],
],
),
const SizedBox(height: 4),
// Métadonnées (expéditeur, date)
Row(
children: [
if (notification.expediteurNom != null) ...[
Text(
notification.expediteurNom!,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
Text(
'',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.5),
),
),
],
Text(
notification.tempsEcoule,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
if (isExpired) ...[
Text(
' • Expirée',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.w500,
),
),
],
],
),
],
),
),
// Menu d'actions
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: AppColors.onSurface.withOpacity(0.6),
size: 20,
),
onSelected: (value) => _handleMenuAction(value),
itemBuilder: (context) => [
if (!notification.estLue)
const PopupMenuItem(
value: 'mark_read',
child: ListTile(
leading: Icon(Icons.mark_email_read, size: 20),
title: Text('Marquer comme lu'),
contentPadding: EdgeInsets.zero,
),
),
PopupMenuItem(
value: 'mark_important',
child: ListTile(
leading: Icon(
notification.estImportante ? Icons.star : Icons.star_border,
size: 20,
),
title: Text(
notification.estImportante
? 'Retirer des importantes'
: 'Marquer comme importante',
),
contentPadding: EdgeInsets.zero,
),
),
if (!notification.estArchivee)
const PopupMenuItem(
value: 'archive',
child: ListTile(
leading: Icon(Icons.archive, size: 20),
title: Text('Archiver'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'delete',
child: ListTile(
leading: Icon(Icons.delete, size: 20, color: Colors.red),
title: Text('Supprimer', style: TextStyle(color: Colors.red)),
contentPadding: EdgeInsets.zero,
),
),
],
),
],
),
const SizedBox(height: 12),
// Message de la notification
Text(
notification.messageAffichage,
style: AppTextStyles.bodyMedium.copyWith(
color: isExpired
? AppColors.onSurface.withOpacity(0.6)
: AppColors.onSurface.withOpacity(0.8),
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
// Image de la notification (si présente)
if (notification.imageUrl != null) ...[
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
notification.imageUrl!,
height: 120,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
height: 120,
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.image_not_supported,
color: AppColors.onSurface.withOpacity(0.5),
),
),
),
),
],
// Actions rapides
if (notification.hasActionsRapides) ...[
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: notification.actionsRapidesActives
.take(3) // Limite à 3 actions pour éviter l'encombrement
.map((action) => _buildActionButton(action))
.toList(),
),
],
// Tags (si présents)
if (notification.tags != null && notification.tags!.isNotEmpty) ...[
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 6,
children: notification.tags!
.take(3) // Limite à 3 tags
.map((tag) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Text(
tag,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
))
.toList(),
),
],
],
),
),
),
);
}
Widget _buildActionButton(ActionNotification action) {
return OutlinedButton.icon(
onPressed: () => onActionTap?.call(action),
icon: Icon(
_getActionIcon(action.icone),
size: 16,
),
label: Text(
action.libelle,
style: AppTextStyles.labelMedium,
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
foregroundColor: action.couleur != null
? Color(int.parse(action.couleur!.replaceFirst('#', '0xFF')))
: AppColors.primary,
side: BorderSide(
color: action.couleur != null
? Color(int.parse(action.couleur!.replaceFirst('#', '0xFF')))
: AppColors.primary,
),
),
);
}
Color _getTypeColor() {
try {
return Color(int.parse(notification.couleurType.replaceFirst('#', '0xFF')));
} catch (e) {
return AppColors.primary;
}
}
IconData _getTypeIcon() {
switch (notification.typeNotification.icone) {
case 'event':
return Icons.event;
case 'payment':
return Icons.payment;
case 'help':
return Icons.help;
case 'person_add':
return Icons.person_add;
case 'cake':
return Icons.cake;
case 'campaign':
return Icons.campaign;
case 'mail':
return Icons.mail;
case 'system_update':
return Icons.system_update;
case 'build':
return Icons.build;
case 'schedule':
return Icons.schedule;
case 'event_busy':
return Icons.event_busy;
case 'check_circle':
return Icons.check_circle;
case 'paid':
return Icons.paid;
case 'error':
return Icons.error;
case 'thumb_up':
return Icons.thumb_up;
case 'volunteer_activism':
return Icons.volunteer_activism;
case 'groups':
return Icons.groups;
case 'alternate_email':
return Icons.alternate_email;
default:
return Icons.notifications;
}
}
IconData _getActionIcon(String? iconeName) {
if (iconeName == null) return Icons.touch_app;
switch (iconeName) {
case 'visibility':
return Icons.visibility;
case 'event_available':
return Icons.event_available;
case 'directions':
return Icons.directions;
case 'payment':
return Icons.payment;
case 'schedule':
return Icons.schedule;
case 'receipt':
return Icons.receipt;
case 'person':
return Icons.person;
case 'message':
return Icons.message;
case 'phone':
return Icons.phone;
case 'reply':
return Icons.reply;
default:
return Icons.touch_app;
}
}
void _handleMenuAction(String action) {
switch (action) {
case 'mark_read':
onMarkAsRead?.call();
break;
case 'mark_important':
onMarkAsImportant?.call();
break;
case 'archive':
onArchive?.call();
break;
case 'delete':
onDelete?.call();
break;
}
}
}

View File

@@ -1,389 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../domain/entities/notification.dart';
/// Widget de recherche et filtrage des notifications
class NotificationSearchWidget extends StatefulWidget {
final Function(String) onSearchChanged;
final Function({
Set<TypeNotification>? types,
Set<StatutNotification>? statuts,
}) onFiltersChanged;
final Set<TypeNotification> selectedTypes;
final Set<StatutNotification> selectedStatuts;
const NotificationSearchWidget({
super.key,
required this.onSearchChanged,
required this.onFiltersChanged,
required this.selectedTypes,
required this.selectedStatuts,
});
@override
State<NotificationSearchWidget> createState() => _NotificationSearchWidgetState();
}
class _NotificationSearchWidgetState extends State<NotificationSearchWidget> {
final TextEditingController _searchController = TextEditingController();
bool _showFilters = false;
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return UnifiedCard(
variant: UnifiedCardVariant.outlined,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Barre de recherche
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher dans les notifications...',
hintStyle: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface.withOpacity(0.6),
),
prefixIcon: Icon(
Icons.search,
color: AppColors.onSurface.withOpacity(0.6),
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
widget.onSearchChanged('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.outline.withOpacity(0.3),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.outline.withOpacity(0.3),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.primary,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: widget.onSearchChanged,
),
),
const SizedBox(width: 12),
// Bouton de filtres
IconButton(
onPressed: () {
setState(() {
_showFilters = !_showFilters;
});
},
icon: Icon(
Icons.filter_list,
color: _hasActiveFilters()
? AppColors.primary
: AppColors.onSurface.withOpacity(0.6),
),
style: IconButton.styleFrom(
backgroundColor: _hasActiveFilters()
? AppColors.primary.withOpacity(0.1)
: AppColors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: _hasActiveFilters()
? AppColors.primary
: AppColors.outline.withOpacity(0.3),
),
),
),
),
],
),
// Panneau de filtres (conditionnel)
if (_showFilters) ...[
const SizedBox(height: 16),
_buildFiltersPanel(),
],
],
),
),
);
}
Widget _buildFiltersPanel() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête des filtres
Row(
children: [
Text(
'Filtres',
style: AppTextStyles.titleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
if (_hasActiveFilters())
TextButton(
onPressed: _clearAllFilters,
child: Text(
'Tout effacer',
style: AppTextStyles.labelMedium.copyWith(
color: AppColors.primary,
),
),
),
],
),
const SizedBox(height: 12),
// Filtres par type
Text(
'Types de notification',
style: AppTextStyles.labelLarge.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _getPopularTypes()
.map((type) => _buildTypeChip(type))
.toList(),
),
const SizedBox(height: 16),
// Filtres par statut
Text(
'Statuts',
style: AppTextStyles.labelLarge.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _getPopularStatuts()
.map((statut) => _buildStatutChip(statut))
.toList(),
),
const SizedBox(height: 16),
// Boutons d'action
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
setState(() {
_showFilters = false;
});
},
child: const Text('Fermer'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
_showFilters = false;
});
// Les filtres sont déjà appliqués en temps réel
},
child: const Text('Appliquer'),
),
),
],
),
],
);
}
Widget _buildTypeChip(TypeNotification type) {
final isSelected = widget.selectedTypes.contains(type);
return FilterChip(
label: Text(
type.libelle,
style: AppTextStyles.labelMedium.copyWith(
color: isSelected ? AppColors.onPrimary : AppColors.onSurface,
),
),
selected: isSelected,
onSelected: (selected) {
final newTypes = Set<TypeNotification>.from(widget.selectedTypes);
if (selected) {
newTypes.add(type);
} else {
newTypes.remove(type);
}
widget.onFiltersChanged(types: newTypes);
},
selectedColor: AppColors.primary,
backgroundColor: AppColors.surface,
side: BorderSide(
color: isSelected
? AppColors.primary
: AppColors.outline.withOpacity(0.3),
),
avatar: isSelected
? null
: Icon(
_getTypeIcon(type),
size: 16,
color: _getTypeColor(type),
),
);
}
Widget _buildStatutChip(StatutNotification statut) {
final isSelected = widget.selectedStatuts.contains(statut);
return FilterChip(
label: Text(
statut.libelle,
style: AppTextStyles.labelMedium.copyWith(
color: isSelected ? AppColors.onPrimary : AppColors.onSurface,
),
),
selected: isSelected,
onSelected: (selected) {
final newStatuts = Set<StatutNotification>.from(widget.selectedStatuts);
if (selected) {
newStatuts.add(statut);
} else {
newStatuts.remove(statut);
}
widget.onFiltersChanged(statuts: newStatuts);
},
selectedColor: AppColors.primary,
backgroundColor: AppColors.surface,
side: BorderSide(
color: isSelected
? AppColors.primary
: AppColors.outline.withOpacity(0.3),
),
avatar: isSelected
? null
: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: _getStatutColor(statut),
shape: BoxShape.circle,
),
),
);
}
List<TypeNotification> _getPopularTypes() {
return [
TypeNotification.nouvelEvenement,
TypeNotification.cotisationDue,
TypeNotification.nouvelleDemandeAide,
TypeNotification.nouveauMembre,
TypeNotification.annonceGenerale,
TypeNotification.messagePrive,
];
}
List<StatutNotification> _getPopularStatuts() {
return [
StatutNotification.nonLue,
StatutNotification.lue,
StatutNotification.marqueeImportante,
StatutNotification.archivee,
];
}
IconData _getTypeIcon(TypeNotification type) {
switch (type.icone) {
case 'event':
return Icons.event;
case 'payment':
return Icons.payment;
case 'help':
return Icons.help;
case 'person_add':
return Icons.person_add;
case 'campaign':
return Icons.campaign;
case 'mail':
return Icons.mail;
default:
return Icons.notifications;
}
}
Color _getTypeColor(TypeNotification type) {
try {
return Color(int.parse(type.couleur.replaceFirst('#', '0xFF')));
} catch (e) {
return AppColors.primary;
}
}
Color _getStatutColor(StatutNotification statut) {
try {
return Color(int.parse(statut.couleur.replaceFirst('#', '0xFF')));
} catch (e) {
return AppColors.primary;
}
}
bool _hasActiveFilters() {
return widget.selectedTypes.isNotEmpty || widget.selectedStatuts.isNotEmpty;
}
void _clearAllFilters() {
widget.onFiltersChanged(
types: <TypeNotification>{},
statuts: <StatutNotification>{},
);
}
}

View File

@@ -1,400 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
/// Widget d'affichage des statistiques de notifications
class NotificationStatsWidget extends StatelessWidget {
final int totalCount;
final int unreadCount;
final int importantCount;
const NotificationStatsWidget({
super.key,
required this.totalCount,
required this.unreadCount,
required this.importantCount,
});
@override
Widget build(BuildContext context) {
return UnifiedCard(
variant: UnifiedCardVariant.filled,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Statistique principale - Non lues
Expanded(
child: _buildStatItem(
icon: Icons.mark_email_unread,
label: 'Non lues',
value: unreadCount.toString(),
color: unreadCount > 0 ? AppColors.primary : AppColors.onSurface.withOpacity(0.6),
isHighlighted: unreadCount > 0,
),
),
// Séparateur
Container(
width: 1,
height: 40,
color: AppColors.outline.withOpacity(0.2),
),
// Statistique secondaire - Importantes
Expanded(
child: _buildStatItem(
icon: Icons.star,
label: 'Importantes',
value: importantCount.toString(),
color: importantCount > 0 ? AppColors.warning : AppColors.onSurface.withOpacity(0.6),
isHighlighted: importantCount > 0,
),
),
// Séparateur
Container(
width: 1,
height: 40,
color: AppColors.outline.withOpacity(0.2),
),
// Statistique tertiaire - Total
Expanded(
child: _buildStatItem(
icon: Icons.notifications,
label: 'Total',
value: totalCount.toString(),
color: AppColors.onSurface.withOpacity(0.8),
isHighlighted: false,
),
),
],
),
),
);
}
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required Color color,
required bool isHighlighted,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Icône avec badge si mis en évidence
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 18,
),
),
if (isHighlighted && value != '0')
Positioned(
right: -4,
top: -4,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.surface,
width: 2,
),
),
),
),
],
),
const SizedBox(height: 8),
// Valeur
Text(
value,
style: AppTextStyles.titleMedium.copyWith(
color: color,
fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w600,
),
),
const SizedBox(height: 2),
// Label
Text(
label,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
);
}
}
/// Widget d'affichage des statistiques détaillées
class DetailedNotificationStatsWidget extends StatelessWidget {
final Map<String, dynamic> stats;
const DetailedNotificationStatsWidget({
super.key,
required this.stats,
});
@override
Widget build(BuildContext context) {
final totalNotifications = stats['total'] ?? 0;
final unreadNotifications = stats['unread'] ?? 0;
final importantNotifications = stats['important'] ?? 0;
final archivedNotifications = stats['archived'] ?? 0;
final todayNotifications = stats['today'] ?? 0;
final weekNotifications = stats['week'] ?? 0;
final engagementRate = stats['engagement_rate'] ?? 0.0;
return UnifiedCard(
variant: UnifiedCardVariant.outlined,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête
Row(
children: [
Icon(
Icons.analytics,
color: AppColors.primary,
size: 24,
),
const SizedBox(width: 12),
Text(
'Statistiques détaillées',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 20),
// Grille de statistiques
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 2.5,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
children: [
_buildDetailedStatCard(
'Total',
totalNotifications.toString(),
Icons.notifications,
AppColors.primary,
),
_buildDetailedStatCard(
'Non lues',
unreadNotifications.toString(),
Icons.mark_email_unread,
AppColors.warning,
),
_buildDetailedStatCard(
'Importantes',
importantNotifications.toString(),
Icons.star,
AppColors.error,
),
_buildDetailedStatCard(
'Archivées',
archivedNotifications.toString(),
Icons.archive,
AppColors.onSurface.withOpacity(0.6),
),
],
),
const SizedBox(height: 20),
// Statistiques temporelles
Row(
children: [
Expanded(
child: _buildTimeStatCard(
'Aujourd\'hui',
todayNotifications.toString(),
Icons.today,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildTimeStatCard(
'Cette semaine',
weekNotifications.toString(),
Icons.date_range,
),
),
],
),
const SizedBox(height: 20),
// Taux d'engagement
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.primary.withOpacity(0.2),
),
),
child: Row(
children: [
Icon(
Icons.trending_up,
color: AppColors.primary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Taux d\'engagement',
style: AppTextStyles.labelMedium.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Pourcentage de notifications ouvertes',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
],
),
),
Text(
'${engagementRate.toStringAsFixed(1)}%',
style: AppTextStyles.titleMedium.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
),
);
}
Widget _buildDetailedStatCard(
String label,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: color.withOpacity(0.2),
),
),
child: Row(
children: [
Icon(
icon,
color: color,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
value,
style: AppTextStyles.titleSmall.copyWith(
color: color,
fontWeight: FontWeight.w700,
),
),
Text(
label,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
],
),
),
],
),
);
}
Widget _buildTimeStatCard(String label, String value, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.outline.withOpacity(0.2),
),
),
child: Column(
children: [
Icon(
icon,
color: AppColors.onSurfaceVariant,
size: 20,
),
const SizedBox(height: 8),
Text(
value,
style: AppTextStyles.titleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
label,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
),
);
}
}