af5ecaeee1
- **Refonte du démarrage** : Mise en place d'un `AppInitializer` pour gérer l'initialisation asynchrone de Firebase et du cache en arrière-plan, réduisant le travail synchrone au lancement.
- **Sécurisation de l'authentification** :
- Création d'un `AppStartGate` pour gérer proprement la restauration de la session Firebase Auth et les erreurs potentielles sur le Web.
- Amélioration du `LocalUserProvider` avec un "bootstrap léger" permettant de rendre l'UID disponible immédiatement avant le chargement complet du profil.
- Ajout de protections contre les erreurs d'accès à `FirebaseAuth.instance` (notamment pour les problèmes d'interop JS sur le Web).
- **Optimisation de l'UI** :
- Remplacement du `AutoLoginWrapper` par une gestion plus robuste de la navigation post-authentification.
- Amélioration de l'`AuthGuard` pour permettre l'affichage de certains écrans (comme le calendrier) pendant le chargement des données utilisateur (`allowWhileLoading`).
- Ajout d'un écran de splash screen uniformisé (`StartupSplashScreen`).
- **Services & Cache** :
- Introduction de `CacheService` utilisant `shared_preferences` pour le stockage local léger.
- Refactoring des services (`AlertService`, `EmailService`, `FirebaseStorageManager`) pour accéder aux instances Firebase de manière plus flexible via des getters.
- Mise à jour des dépendances dans `pubspec.yaml` pour inclure `shared_preferences`.
- **Calendrier** : Ajout d'une logique de chargement initial différé des événements (`_scheduleInitialEventsLoad`) pour éviter les appels redondants au démarrage.
- **Maintenance** : Mise à jour de la version de l'application à `1.1.23` et nettoyage des fichiers de cache de déploiement.
256 lines
7.8 KiB
Dart
256 lines
7.8 KiB
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import '../models/alert_model.dart';
|
|
import '../utils/debug_log.dart';
|
|
import 'api_service.dart' show FirebaseFunctionsApiService;
|
|
/// Service de gestion des alertes
|
|
/// Architecture simplifiée : le client appelle uniquement les Cloud Functions
|
|
/// Toute la logique métier est gérée côté backend
|
|
class AlertService {
|
|
FirebaseFirestore get _firestore => FirebaseFirestore.instance;
|
|
FirebaseAuth get _auth => FirebaseAuth.instance;
|
|
|
|
/// Stream des alertes pour l'utilisateur connecté
|
|
Stream<List<AlertModel>> getAlertsStream() {
|
|
final user = _auth.currentUser;
|
|
if (user == null) {
|
|
DebugLog.info('[AlertService] Pas d\'utilisateur connecté');
|
|
return Stream.value([]);
|
|
}
|
|
|
|
DebugLog.info('[AlertService] Stream alertes pour utilisateur: ${user.uid}');
|
|
|
|
return _firestore
|
|
.collection('alerts')
|
|
.where('assignedTo', arrayContains: user.uid)
|
|
.where('status', isEqualTo: 'ACTIVE')
|
|
.orderBy('createdAt', descending: true)
|
|
.snapshots()
|
|
.map((snapshot) {
|
|
final alerts = snapshot.docs
|
|
.map((doc) => AlertModel.fromFirestore(doc))
|
|
.toList();
|
|
|
|
DebugLog.info('[AlertService] ${alerts.length} alertes actives');
|
|
return alerts;
|
|
});
|
|
}
|
|
|
|
/// Récupère les alertes non lues
|
|
Future<List<AlertModel>> getUnreadAlerts() async {
|
|
final user = _auth.currentUser;
|
|
if (user == null) return [];
|
|
|
|
try {
|
|
final snapshot = await _firestore
|
|
.collection('alerts')
|
|
.where('assignedTo', arrayContains: user.uid)
|
|
.where('isRead', isEqualTo: false)
|
|
.where('status', isEqualTo: 'ACTIVE')
|
|
.orderBy('createdAt', descending: true)
|
|
.get();
|
|
|
|
return snapshot.docs
|
|
.map((doc) => AlertModel.fromFirestore(doc))
|
|
.toList();
|
|
} catch (e) {
|
|
DebugLog.error('[AlertService] Erreur récupération alertes', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Marque une alerte comme lue
|
|
Future<void> markAsRead(String alertId) async {
|
|
try {
|
|
await _firestore.collection('alerts').doc(alertId).update({
|
|
'isRead': true,
|
|
'readAt': FieldValue.serverTimestamp(),
|
|
});
|
|
DebugLog.info('[AlertService] Alerte $alertId marquée comme lue');
|
|
} catch (e) {
|
|
DebugLog.error('[AlertService] Erreur marquage alerte', e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Marque toutes les alertes comme lues
|
|
Future<void> markAllAsRead() async {
|
|
final user = _auth.currentUser;
|
|
if (user == null) return;
|
|
|
|
try {
|
|
final snapshot = await _firestore
|
|
.collection('alerts')
|
|
.where('assignedTo', arrayContains: user.uid)
|
|
.where('isRead', isEqualTo: false)
|
|
.get();
|
|
|
|
final batch = _firestore.batch();
|
|
for (var doc in snapshot.docs) {
|
|
batch.update(doc.reference, {
|
|
'isRead': true,
|
|
'readAt': FieldValue.serverTimestamp(),
|
|
});
|
|
}
|
|
|
|
await batch.commit();
|
|
DebugLog.info('[AlertService] ${snapshot.docs.length} alertes marquées comme lues');
|
|
} catch (e) {
|
|
DebugLog.error('[AlertService] Erreur marquage alertes', e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Archive une alerte
|
|
Future<void> archiveAlert(String alertId) async {
|
|
try {
|
|
await _firestore.collection('alerts').doc(alertId).update({
|
|
'status': 'ARCHIVED',
|
|
'archivedAt': FieldValue.serverTimestamp(),
|
|
});
|
|
DebugLog.info('[AlertService] Alerte $alertId archivée');
|
|
} catch (e) {
|
|
DebugLog.error('[AlertService] Erreur archivage alerte', e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Crée une alerte manuelle (appelée par l'utilisateur)
|
|
/// Cette méthode appelle la Cloud Function createAlert
|
|
Future<String> createManualAlert({
|
|
required AlertType type,
|
|
required AlertSeverity severity,
|
|
required String message,
|
|
String? title,
|
|
String? equipmentId,
|
|
String? eventId,
|
|
String? actionUrl,
|
|
Map<String, dynamic>? metadata,
|
|
}) async {
|
|
try {
|
|
DebugLog.info('[AlertService] === CRÉATION ALERTE MANUELLE ===');
|
|
DebugLog.info('[AlertService] Type: $type');
|
|
DebugLog.info('[AlertService] Severity: $severity');
|
|
|
|
final apiService = FirebaseFunctionsApiService();
|
|
final result = await apiService.call(
|
|
'createAlert',
|
|
{
|
|
'type': alertTypeToString(type),
|
|
'severity': severity.name.toUpperCase(),
|
|
'title': title,
|
|
'message': message,
|
|
'equipmentId': equipmentId,
|
|
'eventId': eventId,
|
|
'actionUrl': actionUrl,
|
|
'metadata': metadata ?? {},
|
|
},
|
|
);
|
|
|
|
final alertId = result['alertId'] as String;
|
|
DebugLog.info('[AlertService] ✓ Alerte créée: $alertId');
|
|
|
|
return alertId;
|
|
} catch (e, stackTrace) {
|
|
DebugLog.error('[AlertService] ❌ Erreur création alerte', e);
|
|
DebugLog.error('[AlertService] Stack', stackTrace);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Stream des alertes pour un utilisateur spécifique
|
|
Stream<List<AlertModel>> alertsStreamForUser(String userId) {
|
|
return _firestore
|
|
.collection('alerts')
|
|
.where('assignedTo', arrayContains: userId)
|
|
.where('status', isEqualTo: 'ACTIVE')
|
|
.orderBy('createdAt', descending: true)
|
|
.snapshots()
|
|
.map((snapshot) => snapshot.docs
|
|
.map((doc) => AlertModel.fromFirestore(doc))
|
|
.toList());
|
|
}
|
|
|
|
/// Récupère les alertes pour un utilisateur
|
|
Future<List<AlertModel>> getAlertsForUser(String userId) async {
|
|
try {
|
|
final snapshot = await _firestore
|
|
.collection('alerts')
|
|
.where('assignedTo', arrayContains: userId)
|
|
.where('status', isEqualTo: 'ACTIVE')
|
|
.orderBy('createdAt', descending: true)
|
|
.get();
|
|
|
|
return snapshot.docs
|
|
.map((doc) => AlertModel.fromFirestore(doc))
|
|
.toList();
|
|
} catch (e) {
|
|
DebugLog.error('[AlertService] Erreur récupération alertes', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Stream du nombre d'alertes non lues pour un utilisateur
|
|
Stream<int> unreadCountStreamForUser(String userId) {
|
|
return _firestore
|
|
.collection('alerts')
|
|
.where('assignedTo', arrayContains: userId)
|
|
.where('isRead', isEqualTo: false)
|
|
.where('status', isEqualTo: 'ACTIVE')
|
|
.snapshots()
|
|
.map((snapshot) => snapshot.docs.length);
|
|
}
|
|
|
|
/// Supprime une alerte
|
|
Future<void> deleteAlert(String alertId) async {
|
|
try {
|
|
await _firestore.collection('alerts').doc(alertId).delete();
|
|
DebugLog.info('[AlertService] Alerte $alertId supprimée');
|
|
} catch (e) {
|
|
DebugLog.error('[AlertService] Erreur suppression alerte', e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Crée une alerte de création d'événement
|
|
Future<void> createEventCreatedAlert({
|
|
required String eventId,
|
|
required String eventName,
|
|
required DateTime eventDate,
|
|
}) async {
|
|
await createManualAlert(
|
|
type: AlertType.eventCreated,
|
|
severity: AlertSeverity.info,
|
|
message: 'Nouvel événement créé: "$eventName" le ${_formatDate(eventDate)}',
|
|
eventId: eventId,
|
|
metadata: {
|
|
'eventName': eventName,
|
|
'eventDate': eventDate.toIso8601String(),
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Crée une alerte de modification d'événement
|
|
Future<void> createEventModifiedAlert({
|
|
required String eventId,
|
|
required String eventName,
|
|
required String modification,
|
|
}) async {
|
|
await createManualAlert(
|
|
type: AlertType.eventModified,
|
|
severity: AlertSeverity.info,
|
|
message: 'Événement "$eventName" modifié: $modification',
|
|
eventId: eventId,
|
|
metadata: {
|
|
'eventName': eventName,
|
|
'modification': modification,
|
|
},
|
|
);
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
return '${date.day}/${date.month}/${date.year}';
|
|
}
|
|
}
|
|
|