Cette mise à jour introduit la gestion complète des utilisateurs (création, mise à jour, suppression) via des Cloud Functions et optimise de manière significative le chargement des données dans toute l'application.
**Features :**
- **Gestion des utilisateurs (Backend & Frontend) :**
- Ajout des Cloud Functions `getUser`, `updateUser` et `deleteUser` pour gérer les utilisateurs de manière sécurisée, en respectant les permissions des rôles.
- L'authentification passe désormais par `onCall` pour plus de sécurité.
- **Optimisation du chargement des données :**
- Introduction de nouvelles Cloud Functions `getEquipmentsByIds` et `getContainersByIds` pour récupérer uniquement les documents nécessaires, réduisant ainsi la charge sur le client et Firestore.
- Les fournisseurs (`EquipmentProvider`, `ContainerProvider`) ont été refactorisés pour utiliser un chargement à la demande (`ensureLoaded`) et mettre en cache les données récupérées.
- Les écrans de détails et de préparation d'événements n'utilisent plus de `Stream` globaux, mais chargent les équipements et boites spécifiques via ces nouvelles fonctions, améliorant considérablement les performances.
**Refactorisation et Améliorations :**
- **Backend (Cloud Functions) :**
- Le service de vérification de disponibilité (`checkEquipmentAvailability`) est désormais une Cloud Function, déplaçant la logique métier côté serveur.
- La récupération des données (utilisateurs, événements, alertes) a été centralisée derrière des Cloud Functions, remplaçant les appels directs à Firestore depuis le client.
- Amélioration de la sérialisation des données (timestamps, références) dans les réponses des fonctions.
- **Frontend (Flutter) :**
- `LocalUserProvider` charge désormais les informations de l'utilisateur connecté via la fonction `getCurrentUser`, incluant son rôle et ses permissions en un seul appel.
- `AlertProvider` utilise des fonctions pour charger et manipuler les alertes, abandonnant le `Stream` Firestore.
- `EventAvailabilityService` utilise maintenant la Cloud Function `checkEquipmentAvailability` au lieu d'une logique client complexe.
- Correction de la gestion des références de rôles (`roles/ADMIN`) et des `DocumentReference` pour les utilisateurs dans l'ensemble de l'application.
- Le service d'export ICS (`IcsExportService`) a été simplifié, partant du principe que les données nécessaires (utilisateurs, options) sont déjà chargées dans l'application.
243 lines
7.5 KiB
Dart
243 lines
7.5 KiB
Dart
import 'package:em2rp/models/event_model.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
|
|
class IcsExportService {
|
|
/// Génère un fichier ICS à partir d'un événement
|
|
static Future<String> generateIcsContent(EventModel event) async {
|
|
final now = DateTime.now().toUtc();
|
|
final timestamp = DateFormat('yyyyMMddTHHmmss').format(now) + 'Z';
|
|
|
|
// Récupérer les informations supplémentaires
|
|
final eventTypeName = await _getEventTypeName(event.eventTypeId);
|
|
final workforce = await _getWorkforceDetails(event.workforce);
|
|
final optionsWithNames = await _getOptionsDetails(event.options);
|
|
|
|
// Formater les dates au format ICS (UTC)
|
|
final startDate = _formatDateForIcs(event.startDateTime);
|
|
final endDate = _formatDateForIcs(event.endDateTime);
|
|
|
|
// Construire la description détaillée
|
|
final description = _buildDescription(event, eventTypeName, workforce, optionsWithNames);
|
|
|
|
// Générer un UID unique basé sur l'ID de l'événement
|
|
final uid = 'em2rp-${event.id}@em2rp.app';
|
|
|
|
// Construire le contenu ICS
|
|
final icsContent = '''BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//EM2RP//Event Manager//FR
|
|
CALSCALE:GREGORIAN
|
|
METHOD:PUBLISH
|
|
BEGIN:VEVENT
|
|
UID:$uid
|
|
DTSTAMP:$timestamp
|
|
DTSTART:$startDate
|
|
DTEND:$endDate
|
|
SUMMARY:${_escapeIcsText(event.name)}
|
|
DESCRIPTION:${_escapeIcsText(description)}
|
|
LOCATION:${_escapeIcsText(event.address)}
|
|
STATUS:${_getEventStatus(event.status)}
|
|
CATEGORIES:${_escapeIcsText(eventTypeName)}
|
|
END:VEVENT
|
|
END:VCALENDAR''';
|
|
|
|
return icsContent;
|
|
}
|
|
|
|
/// Récupère le nom du type d'événement depuis EventModel (déjà chargé)
|
|
/// Note: Les eventTypes sont maintenant chargés via Cloud Function dans l'EventModel
|
|
static Future<String> _getEventTypeName(String eventTypeId) async {
|
|
if (eventTypeId.isEmpty) return 'Non spécifié';
|
|
|
|
// Les eventTypes sont publics et déjà chargés dans l'app via Cloud Function
|
|
// On retourne simplement l'ID, le nom sera résolu par l'app
|
|
return eventTypeId;
|
|
}
|
|
|
|
/// Récupère les détails de la main d'œuvre
|
|
/// Note: Les données users devraient être passées directement depuis l'app
|
|
/// qui les a déjà récupérées via Cloud Function
|
|
static Future<List<String>> _getWorkforceDetails(List<dynamic> workforce) async {
|
|
final List<String> workforceNames = [];
|
|
|
|
for (final ref in workforce) {
|
|
try {
|
|
// Si c'est déjà une Map avec les données, l'utiliser directement
|
|
if (ref is Map<String, dynamic>) {
|
|
final firstName = ref['firstName'] ?? '';
|
|
final lastName = ref['lastName'] ?? '';
|
|
if (firstName.isNotEmpty || lastName.isNotEmpty) {
|
|
workforceNames.add('$firstName $lastName'.trim());
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Si c'est un String (UID), on ne peut pas récupérer les données ici
|
|
// Les données devraient être passées directement
|
|
if (ref is String) {
|
|
workforceNames.add('Utilisateur $ref');
|
|
continue;
|
|
}
|
|
|
|
// Si c'est une DocumentReference, extraire l'ID seulement
|
|
if (ref is DocumentReference) {
|
|
workforceNames.add('Utilisateur ${ref.id}');
|
|
}
|
|
} catch (e) {
|
|
print('Erreur lors du traitement des détails utilisateur: $e');
|
|
}
|
|
}
|
|
|
|
return workforceNames;
|
|
}
|
|
|
|
/// Récupère les détails des options
|
|
/// Note: Les options sont publiques et déjà chargées via Cloud Function
|
|
static Future<List<Map<String, dynamic>>> _getOptionsDetails(List<Map<String, dynamic>> options) async {
|
|
final List<Map<String, dynamic>> optionsWithNames = [];
|
|
|
|
for (final option in options) {
|
|
try {
|
|
// Les options devraient déjà contenir le nom
|
|
optionsWithNames.add({
|
|
'name': option['name'] ?? option['optionId'] ?? 'Option inconnue',
|
|
'quantity': option['quantity'],
|
|
});
|
|
} catch (e) {
|
|
print('Erreur lors du traitement des options: $e');
|
|
}
|
|
}
|
|
|
|
return optionsWithNames;
|
|
}
|
|
|
|
/// Construit la description détaillée de l'événement
|
|
static String _buildDescription(
|
|
EventModel event,
|
|
String eventTypeName,
|
|
List<String> workforce,
|
|
List<Map<String, dynamic>> optionsWithNames,
|
|
) {
|
|
final buffer = StringBuffer();
|
|
|
|
// Type d'événement
|
|
buffer.writeln('TYPE: $eventTypeName');
|
|
buffer.writeln('');
|
|
|
|
// Description
|
|
if (event.description.isNotEmpty) {
|
|
buffer.writeln('DESCRIPTION:');
|
|
buffer.writeln(event.description);
|
|
buffer.writeln('');
|
|
}
|
|
|
|
// Jauge
|
|
if (event.jauge != null) {
|
|
buffer.writeln('JAUGE: ${event.jauge} personnes');
|
|
}
|
|
|
|
// Contact email
|
|
if (event.contactEmail != null && event.contactEmail!.isNotEmpty) {
|
|
buffer.writeln('EMAIL DE CONTACT: ${event.contactEmail}');
|
|
}
|
|
|
|
// Contact téléphone
|
|
if (event.contactPhone != null && event.contactPhone!.isNotEmpty) {
|
|
buffer.writeln('TÉLÉPHONE DE CONTACT: ${event.contactPhone}');
|
|
}
|
|
|
|
// Adresse
|
|
if (event.address.isNotEmpty) {
|
|
buffer.writeln('');
|
|
buffer.writeln('ADRESSE: ${event.address}');
|
|
}
|
|
|
|
// Temps d'installation et démontage
|
|
if (event.installationTime > 0 || event.disassemblyTime > 0) {
|
|
buffer.writeln('');
|
|
if (event.installationTime > 0) {
|
|
buffer.writeln('INSTALLATION: ${event.installationTime}h');
|
|
}
|
|
if (event.disassemblyTime > 0) {
|
|
buffer.writeln('DÉMONTAGE: ${event.disassemblyTime}h');
|
|
}
|
|
}
|
|
|
|
// Main d'œuvre
|
|
if (workforce.isNotEmpty) {
|
|
buffer.writeln('');
|
|
buffer.writeln('MAIN D\'ŒUVRE:');
|
|
for (final name in workforce) {
|
|
buffer.writeln(' - $name');
|
|
}
|
|
}
|
|
|
|
// Options
|
|
if (optionsWithNames.isNotEmpty) {
|
|
buffer.writeln('');
|
|
buffer.writeln('OPTIONS:');
|
|
for (final option in optionsWithNames) {
|
|
final optionName = option['name'] ?? 'Option inconnue';
|
|
final quantity = option['quantity'];
|
|
if (quantity != null && quantity > 1) {
|
|
buffer.writeln(' - $optionName (x$quantity)');
|
|
} else {
|
|
buffer.writeln(' - $optionName');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prix
|
|
if (event.basePrice > 0) {
|
|
buffer.writeln('');
|
|
buffer.writeln('PRIX DE BASE: ${event.basePrice.toStringAsFixed(2)}€');
|
|
}
|
|
|
|
// Lien vers l'application
|
|
buffer.writeln('');
|
|
buffer.writeln('---');
|
|
buffer.writeln('Géré par EM2RP Event Manager');
|
|
|
|
return buffer.toString();
|
|
}
|
|
|
|
/// Formate une date au format ICS (yyyyMMddTHHmmssZ)
|
|
static String _formatDateForIcs(DateTime dateTime) {
|
|
final utcDate = dateTime.toUtc();
|
|
return DateFormat('yyyyMMddTHHmmss').format(utcDate) + 'Z';
|
|
}
|
|
|
|
/// Échappe les caractères spéciaux pour le format ICS
|
|
static String _escapeIcsText(String text) {
|
|
return text
|
|
.replaceAll('\\', '\\\\')
|
|
.replaceAll(',', '\\,')
|
|
.replaceAll(';', '\\;')
|
|
.replaceAll('\n', '\\n')
|
|
.replaceAll('\r', '');
|
|
}
|
|
|
|
/// Convertit le statut de l'événement en statut ICS
|
|
static String _getEventStatus(EventStatus status) {
|
|
switch (status) {
|
|
case EventStatus.confirmed:
|
|
return 'CONFIRMED';
|
|
case EventStatus.canceled:
|
|
return 'CANCELLED';
|
|
case EventStatus.waitingForApproval:
|
|
return 'TENTATIVE';
|
|
}
|
|
}
|
|
|
|
/// Génère le nom du fichier ICS
|
|
static String generateFileName(EventModel event) {
|
|
final safeName = event.name
|
|
.replaceAll(RegExp(r'[^\w\s-]'), '')
|
|
.replaceAll(RegExp(r'\s+'), '_');
|
|
final dateStr = DateFormat('yyyyMMdd').format(event.startDateTime);
|
|
return 'event_${safeName}_$dateStr.ics';
|
|
}
|
|
}
|
|
|