Files
unionflow-client-quarkus-pr…/unionflow-mobile-apps/lib/features/notifications/presentation/pages/notification_preferences_page.dart
2025-09-17 17:54:06 +00:00

780 lines
26 KiB
Dart

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'),
),
],
),
);
}
}