Versione OK Pour l'onglet événements.
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user