Files
EM2_ERP/em2rp/lib/services/event_form_service.dart
ElPoyo beaabceda4 feat: Intégration d'un système complet d'alertes et de notifications par email
Cette mise à jour majeure introduit un système de notifications robuste, centré sur la création d'alertes et l'envoi d'emails via des Cloud Functions. Elle inclut la gestion des préférences utilisateur, la création automatique d'alertes lors d'événements critiques et une nouvelle interface dédiée.

**Backend (Cloud Functions) :**
- **Nouveau service d'alerting (`createAlert`, `processEquipmentValidation`) :**
    - `createAlert` : Nouvelle fonction pour créer une alerte. Elle détermine les utilisateurs à notifier (admins, workforce d'événement) et gère la persistance dans Firestore.
    - `processEquipmentValidation` : Endpoint appelé lors de la validation du matériel (chargement/déchargement). Il analyse l'état de l'équipement (`LOST`, `MISSING`, `DAMAGED`) et crée automatiquement les alertes correspondantes.
- **Système d'envoi d'emails (`sendAlertEmail`, `sendDailyDigest`) :**
    - `sendAlertEmail` : Cloud Function `onCall` pour envoyer un email d'alerte individuel. Elle respecte les préférences de notification de l'utilisateur (canal email, type d'alerte).
    - `sendDailyDigest` : Tâche planifiée (tous les jours à 8h) qui envoie un email récapitulatif des alertes non lues des dernières 24 heures aux utilisateurs concernés.
    - Ajout de templates HTML (`base-template`, `alert-individual`, `alert-digest`) avec `Handlebars` pour des emails riches.
    - Configuration centralisée du SMTP via des variables d'environnement (`.env`).
- **Triggers Firestore (`onEventCreated`, `onEventUpdated`) :**
    - Des triggers créent désormais des alertes d'information lorsqu'un événement est créé ou que de nouveaux membres sont ajoutés à la workforce.
- **Règles Firestore :**
    - Mises à jour pour autoriser les utilisateurs authentifiés à créer et modifier leurs propres alertes (marquer comme lue, supprimer), tout en sécurisant les accès.

**Frontend (Flutter) :**
- **Nouvel `AlertService` et `EmailService` :**
    - `AlertService` : Centralise la logique de création, lecture et gestion des alertes côté client en appelant les nouvelles Cloud Functions.
    - `EmailService` : Service pour déclencher l'envoi d'emails via la fonction `sendAlertEmail`. Il contient la logique pour déterminer si une notification doit être immédiate (critique) ou différée (digest).
- **Nouvelle page de Notifications (`/alerts`) :**
    - Interface dédiée pour lister toutes les alertes de l'utilisateur, avec des onglets pour filtrer par catégorie (Toutes, Événement, Maintenance, Équipement).
    - Permet de marquer les alertes comme lues, de les supprimer et de tout marquer comme lu.
- **Intégration dans l'UI :**
    - Ajout d'un badge de notification dans la `CustomAppBar` affichant le nombre d'alertes non lues en temps réel.
    - Le `AutoLoginWrapper` gère désormais la redirection vers des routes profondes (ex: `/alerts`) depuis une URL.
- **Gestion des Préférences de Notification :**
    - Ajout d'un widget `NotificationPreferencesWidget` dans la page "Mon Compte".
    - Les utilisateurs peuvent désormais activer/désactiver les notifications par email, ainsi que filtrer par type d'alerte (événements, maintenance, etc.).
    - Le `UserModel` et `LocalUserProvider` ont été étendus pour gérer ce nouveau modèle de préférences.
- **Création d'alertes contextuelles :**
    - Le service `EventFormService` crée maintenant automatiquement une alerte lorsqu'un événement est créé ou modifié.
    - La page de préparation d'événement (`EventPreparationPage`) appelle `processEquipmentValidation` à la fin de chaque étape pour une détection automatisée des anomalies.

**Dépendances et CI/CD :**
- Ajout des dépendances `cloud_functions` et `timeago` (Flutter), et `nodemailer`, `handlebars`, `dotenv` (Node.js).
- Ajout de scripts de déploiement PowerShell (`deploy_functions.ps1`, `deploy_firestore_rules.ps1`) pour simplifier les mises en production.
2026-01-15 23:15:25 +01:00

237 lines
8.3 KiB
Dart

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:file_picker/file_picker.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/event_type_model.dart';
import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/alert_service.dart';
import 'dart:developer' as developer;
class EventFormService {
static final ApiService _apiService = apiService;
static final DataService _dataService = DataService(FirebaseFunctionsApiService());
// ============================================================================
// READ Operations - Utilise l'API (sécurisé avec permissions côté serveur)
// ============================================================================
static Future<List<EventTypeModel>> fetchEventTypes() async {
developer.log('Fetching event types via API...', name: 'EventFormService');
try {
final eventTypesData = await _dataService.getEventTypes();
final eventTypes = eventTypesData.map((data) => EventTypeModel.fromMap(data, data['id'] as String)).toList();
developer.log('${eventTypes.length} event types loaded.', name: 'EventFormService');
return eventTypes;
} catch (e, s) {
developer.log('Error fetching event types', name: 'EventFormService', error: e, stackTrace: s);
throw Exception("Could not load event types. Please check permissions.");
}
}
static Future<List<UserModel>> fetchUsers() async {
try {
final usersData = await _dataService.getUsers();
return usersData.map((data) => UserModel.fromMap(data, data['id'] as String)).toList();
} catch (e) {
developer.log('Error fetching users', name: 'EventFormService', error: e);
throw Exception("Could not load users.");
}
}
// ============================================================================
// STORAGE - Reste inchangé (déjà via Cloud Function)
// ============================================================================
static Future<List<Map<String, String>>> uploadFiles(List<PlatformFile> files) async {
List<Map<String, String>> uploadedFiles = [];
for (final file in files) {
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes != null) {
final ref = FirebaseStorage.instance.ref().child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
final uploadTask = await ref.putData(fileBytes);
final url = await uploadTask.ref.getDownloadURL();
uploadedFiles.add({'name': fileName, 'url': url});
} else {
throw Exception("Impossible de lire le fichier $fileName");
}
}
return uploadedFiles;
}
static Future<String?> moveEventFileHttp({
required String sourcePath,
required String destinationPath,
}) async {
final url = Uri.parse('https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2');
final user = FirebaseAuth.instance.currentUser;
final idToken = await user?.getIdToken();
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
if (idToken != null) 'Authorization': 'Bearer $idToken',
},
body: jsonEncode({
'data': {
'sourcePath': sourcePath,
'destinationPath': destinationPath,
}
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['url'] != null) {
return data['url'] as String;
} else if (data['result'] != null && data['result']['url'] != null) {
return data['result']['url'] as String;
}
return null;
} else {
print('Erreur Cloud Function: \n${response.body}');
return null;
}
}
// ============================================================================
// CRUD Operations - Utilise le backend sécurisé
// ============================================================================
static Future<String> createEvent(EventModel event) async {
try {
final result = await _apiService.call('createEvent', event.toMap());
final eventId = result['id'] as String;
// NOUVEAU : Créer alerte automatique pour les utilisateurs assignés
try {
await AlertService().createEventCreatedAlert(
eventId: eventId,
eventName: event.name,
eventDate: event.startDateTime,
);
developer.log('Alert created for new event: $eventId', name: 'EventFormService');
} catch (alertError) {
// Ne pas bloquer la création de l'événement si l'alerte échoue
developer.log('Warning: Could not create alert for event',
name: 'EventFormService',
error: alertError);
}
return eventId;
} catch (e) {
developer.log('Error creating event', name: 'EventFormService', error: e);
rethrow;
}
}
static Future<void> updateEvent(EventModel event) async {
try {
if (event.id.isEmpty) {
throw Exception("Cannot update event: Event ID is empty");
}
developer.log('Updating event with ID: ${event.id}', name: 'EventFormService');
final eventData = event.toMap();
eventData['eventId'] = event.id;
await _apiService.call('updateEvent', eventData);
developer.log('Event updated successfully', name: 'EventFormService');
// NOUVEAU : Créer alerte automatique pour les utilisateurs assignés
try {
final currentUserId = FirebaseAuth.instance.currentUser?.uid;
if (currentUserId != null) {
await AlertService().createEventModifiedAlert(
eventId: event.id,
eventName: event.name,
modification: 'Informations modifiées',
);
developer.log('Alert created for modified event: ${event.id}', name: 'EventFormService');
}
} catch (alertError) {
// Ne pas bloquer la modification de l'événement si l'alerte échoue
developer.log('Warning: Could not create alert for event modification',
name: 'EventFormService',
error: alertError);
}
} catch (e) {
developer.log('Error updating event', name: 'EventFormService', error: e);
rethrow;
}
}
static Future<void> deleteEvent(String eventId) async {
try {
await _apiService.call('deleteEvent', {'eventId': eventId});
} catch (e) {
developer.log('Error deleting event', name: 'EventFormService', error: e);
rethrow;
}
}
static Future<List<Map<String, String>>> moveFilesToEvent(
List<Map<String, String>> tempFiles, String eventId) async {
List<Map<String, String>> newFiles = [];
for (final file in tempFiles) {
final fileName = file['name']!;
final oldUrl = file['url']!;
String sourcePath;
final tempPattern = RegExp(r'events/temp/[^?]+');
final match = tempPattern.firstMatch(oldUrl);
if (match != null) {
sourcePath = match.group(0)!;
} else {
final tempFileName = Uri.decodeComponent(oldUrl.split('/').last.split('?').first);
sourcePath = tempFileName;
}
final destinationPath = 'events/$eventId/$fileName';
final newUrl = await moveEventFileHttp(
sourcePath: sourcePath,
destinationPath: destinationPath,
);
if (newUrl != null) {
newFiles.add({'name': fileName, 'url': newUrl});
} else {
newFiles.add({'name': fileName, 'url': oldUrl});
}
}
return newFiles;
}
static Future<void> updateEventDocuments(String eventId, List<Map<String, String>> documents) async {
try {
if (eventId.isEmpty) {
throw Exception("Event ID cannot be empty");
}
developer.log('Updating event documents for ID: $eventId (${documents.length} documents)', name: 'EventFormService');
await _apiService.call('updateEvent', {
'eventId': eventId,
'documents': documents,
});
developer.log('Event documents updated successfully', name: 'EventFormService');
} catch (e) {
developer.log('Error updating event documents', name: 'EventFormService', error: e);
throw Exception("Could not update event documents.");
}
}
}