import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:injectable/injectable.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/cotisation_model.dart'; /// Service de gestion des notifications /// Gère les notifications locales et push pour les cotisations @LazySingleton() class NotificationService { static const String _notificationsEnabledKey = 'notifications_enabled'; static const String _reminderDaysKey = 'reminder_days'; static const String _scheduledNotificationsKey = 'scheduled_notifications'; final FlutterLocalNotificationsPlugin _localNotifications; final SharedPreferences _prefs; NotificationService(this._localNotifications, this._prefs); /// Initialise le service de notifications Future initialize() async { const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await _localNotifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationTapped, ); // Demander les permissions sur iOS await _requestPermissions(); } /// Demande les permissions de notification Future _requestPermissions() async { final result = await _localNotifications .resolvePlatformSpecificImplementation() ?.requestPermissions( alert: true, badge: true, sound: true, ); return result ?? true; } /// Planifie une notification de rappel pour une cotisation Future schedulePaymentReminder(CotisationModel cotisation) async { if (!await isNotificationsEnabled()) return; final reminderDays = await getReminderDays(); final notificationDate = cotisation.dateEcheance.subtract(Duration(days: reminderDays)); // Ne pas planifier si la date est déjà passée if (notificationDate.isBefore(DateTime.now())) return; const androidDetails = AndroidNotificationDetails( 'payment_reminders', 'Rappels de paiement', channelDescription: 'Notifications de rappel pour les cotisations à payer', importance: Importance.high, priority: Priority.high, icon: '@mipmap/ic_launcher', color: Color(0xFF2196F3), playSound: true, enableVibration: true, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); final notificationId = _generateNotificationId(cotisation.id, 'reminder'); await _localNotifications.zonedSchedule( notificationId, 'Rappel de cotisation', 'Votre cotisation ${cotisation.typeCotisation} de ${cotisation.montantDu.toStringAsFixed(0)} XOF arrive à échéance le ${_formatDate(cotisation.dateEcheance)}', _convertToTZDateTime(notificationDate), notificationDetails, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, payload: jsonEncode({ 'type': 'payment_reminder', 'cotisationId': cotisation.id, 'action': 'open_cotisation', }), ); // Sauvegarder la notification planifiée await _saveScheduledNotification(notificationId, cotisation.id, 'reminder', notificationDate); } /// Planifie une notification d'échéance le jour J Future scheduleDueDateNotification(CotisationModel cotisation) async { if (!await isNotificationsEnabled()) return; final notificationDate = DateTime( cotisation.dateEcheance.year, cotisation.dateEcheance.month, cotisation.dateEcheance.day, 9, // 9h du matin ); // Ne pas planifier si la date est déjà passée if (notificationDate.isBefore(DateTime.now())) return; const androidDetails = AndroidNotificationDetails( 'due_date_notifications', 'Échéances du jour', channelDescription: 'Notifications pour les cotisations qui arrivent à échéance', importance: Importance.max, priority: Priority.max, icon: '@mipmap/ic_launcher', color: Color(0xFFFF5722), playSound: true, enableVibration: true, ongoing: true, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, interruptionLevel: InterruptionLevel.critical, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); final notificationId = _generateNotificationId(cotisation.id, 'due_date'); await _localNotifications.zonedSchedule( notificationId, 'Échéance aujourd\'hui !', 'Votre cotisation ${cotisation.typeCotisation} de ${cotisation.montantDu.toStringAsFixed(0)} XOF arrive à échéance aujourd\'hui', _convertToTZDateTime(notificationDate), notificationDetails, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, payload: jsonEncode({ 'type': 'due_date', 'cotisationId': cotisation.id, 'action': 'pay_now', }), ); await _saveScheduledNotification(notificationId, cotisation.id, 'due_date', notificationDate); } /// Envoie une notification immédiate de confirmation de paiement Future showPaymentConfirmation(CotisationModel cotisation, double montantPaye) async { const androidDetails = AndroidNotificationDetails( 'payment_confirmations', 'Confirmations de paiement', channelDescription: 'Notifications de confirmation après paiement', importance: Importance.high, priority: Priority.high, icon: '@mipmap/ic_launcher', color: Color(0xFF4CAF50), playSound: true, enableVibration: true, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _localNotifications.show( _generateNotificationId(cotisation.id, 'payment_success'), 'Paiement confirmé ✅', 'Votre paiement de ${montantPaye.toStringAsFixed(0)} XOF pour la cotisation ${cotisation.typeCotisation} a été confirmé', notificationDetails, payload: jsonEncode({ 'type': 'payment_success', 'cotisationId': cotisation.id, 'action': 'view_receipt', }), ); } /// Envoie une notification d'échec de paiement Future showPaymentFailure(CotisationModel cotisation, String raison) async { const androidDetails = AndroidNotificationDetails( 'payment_failures', 'Échecs de paiement', channelDescription: 'Notifications d\'échec de paiement', importance: Importance.high, priority: Priority.high, icon: '@mipmap/ic_launcher', color: Color(0xFFF44336), playSound: true, enableVibration: true, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _localNotifications.show( _generateNotificationId(cotisation.id, 'payment_failure'), 'Échec de paiement ❌', 'Le paiement pour la cotisation ${cotisation.typeCotisation} a échoué: $raison', notificationDetails, payload: jsonEncode({ 'type': 'payment_failure', 'cotisationId': cotisation.id, 'action': 'retry_payment', }), ); } /// Annule toutes les notifications pour une cotisation Future cancelCotisationNotifications(String cotisationId) async { final scheduledNotifications = await getScheduledNotifications(); final notificationsToCancel = scheduledNotifications .where((n) => n['cotisationId'] == cotisationId) .toList(); for (final notification in notificationsToCancel) { await _localNotifications.cancel(notification['id'] as int); } // Supprimer de la liste des notifications planifiées final updatedNotifications = scheduledNotifications .where((n) => n['cotisationId'] != cotisationId) .toList(); await _prefs.setString(_scheduledNotificationsKey, jsonEncode(updatedNotifications)); } /// Planifie les notifications pour toutes les cotisations actives Future scheduleAllCotisationsNotifications(List cotisations) async { // Annuler toutes les notifications existantes await _localNotifications.cancelAll(); await _clearScheduledNotifications(); // Planifier pour chaque cotisation non payée for (final cotisation in cotisations) { if (!cotisation.isEntierementPayee && !cotisation.isEnRetard) { await schedulePaymentReminder(cotisation); await scheduleDueDateNotification(cotisation); } } } /// Configuration des notifications Future isNotificationsEnabled() async { return _prefs.getBool(_notificationsEnabledKey) ?? true; } Future setNotificationsEnabled(bool enabled) async { await _prefs.setBool(_notificationsEnabledKey, enabled); if (!enabled) { await _localNotifications.cancelAll(); await _clearScheduledNotifications(); } } Future getReminderDays() async { return _prefs.getInt(_reminderDaysKey) ?? 3; // 3 jours par défaut } Future setReminderDays(int days) async { await _prefs.setInt(_reminderDaysKey, days); } Future>> getScheduledNotifications() async { final jsonString = _prefs.getString(_scheduledNotificationsKey); if (jsonString == null) return []; try { final List jsonList = jsonDecode(jsonString); return jsonList.cast>(); } catch (e) { return []; } } /// Méthodes privées void _onNotificationTapped(NotificationResponse response) { if (response.payload != null) { try { final payload = jsonDecode(response.payload!); // TODO: Implémenter la navigation selon l'action // NavigationService.navigateToAction(payload); } catch (e) { // Ignorer les erreurs de parsing } } } int _generateNotificationId(String cotisationId, String type) { return '${cotisationId}_$type'.hashCode; } String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } // Note: Cette méthode nécessite le package timezone // Pour simplifier, on utilise DateTime directement dynamic _convertToTZDateTime(DateTime dateTime) { return dateTime; // Simplification - en production, utiliser TZDateTime } Future _saveScheduledNotification( int notificationId, String cotisationId, String type, DateTime scheduledDate, ) async { final notifications = await getScheduledNotifications(); notifications.add({ 'id': notificationId, 'cotisationId': cotisationId, 'type': type, 'scheduledDate': scheduledDate.toIso8601String(), }); await _prefs.setString(_scheduledNotificationsKey, jsonEncode(notifications)); } Future _clearScheduledNotifications() async { await _prefs.remove(_scheduledNotificationsKey); } }