Files
unionflow-client-quarkus-pr…/unionflow-mobile-apps/lib/core/services/notification_service.dart
2025-09-15 20:15:34 +00:00

363 lines
12 KiB
Dart

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<void> 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<bool> _requestPermissions() async {
final result = await _localNotifications
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
return result ?? true;
}
/// Planifie une notification de rappel pour une cotisation
Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> scheduleAllCotisationsNotifications(List<CotisationModel> 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<bool> isNotificationsEnabled() async {
return _prefs.getBool(_notificationsEnabledKey) ?? true;
}
Future<void> setNotificationsEnabled(bool enabled) async {
await _prefs.setBool(_notificationsEnabledKey, enabled);
if (!enabled) {
await _localNotifications.cancelAll();
await _clearScheduledNotifications();
}
}
Future<int> getReminderDays() async {
return _prefs.getInt(_reminderDaysKey) ?? 3; // 3 jours par défaut
}
Future<void> setReminderDays(int days) async {
await _prefs.setInt(_reminderDaysKey, days);
}
Future<List<Map<String, dynamic>>> getScheduledNotifications() async {
final jsonString = _prefs.getString(_scheduledNotificationsKey);
if (jsonString == null) return [];
try {
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.cast<Map<String, dynamic>>();
} 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<void> _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<void> _clearScheduledNotifications() async {
await _prefs.remove(_scheduledNotificationsKey);
}
}