363 lines
12 KiB
Dart
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);
|
|
}
|
|
}
|