276 lines
8.7 KiB
Dart
276 lines
8.7 KiB
Dart
import 'package:em2rp/config/app_version.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
|
|
///
|
|
/// [eventTypeName] : Nom du type d'événement (optionnel, sera résolu si non fourni)
|
|
/// [userNames] : Map des IDs utilisateurs vers leurs noms complets (optionnel)
|
|
/// [optionNames] : Map des IDs options vers leurs noms (optionnel)
|
|
static Future<String> generateIcsContent(
|
|
EventModel event, {
|
|
String? eventTypeName,
|
|
Map<String, String>? userNames,
|
|
Map<String, String>? optionNames,
|
|
}) async {
|
|
final now = DateTime.now().toUtc();
|
|
final timestamp = '${DateFormat('yyyyMMddTHHmmss').format(now)}Z';
|
|
|
|
// Récupérer les informations supplémentaires
|
|
final resolvedEventTypeName = eventTypeName ?? await _getEventTypeName(event.eventTypeId);
|
|
final workforce = await _getWorkforceDetails(event.workforce, userNames: userNames);
|
|
final optionsWithNames = await _getOptionsDetails(event.options, optionNames: optionNames);
|
|
|
|
// 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, resolvedEventTypeName, 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(resolvedEventTypeName)}
|
|
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
|
|
/// Si userNames est fourni, utilise les noms déjà résolus pour de meilleures performances
|
|
static Future<List<String>> _getWorkforceDetails(
|
|
List<dynamic> workforce, {
|
|
Map<String, String>? userNames,
|
|
}) 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) et qu'on a les noms résolus, les utiliser
|
|
if (ref is String) {
|
|
if (userNames != null && userNames.containsKey(ref)) {
|
|
workforceNames.add(userNames[ref]!);
|
|
} else {
|
|
workforceNames.add('Utilisateur $ref');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Si c'est une DocumentReference
|
|
if (ref is DocumentReference) {
|
|
final userId = ref.id;
|
|
if (userNames != null && userNames.containsKey(userId)) {
|
|
workforceNames.add(userNames[userId]!);
|
|
} else {
|
|
workforceNames.add('Utilisateur $userId');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('Erreur lors du traitement des détails utilisateur: $e');
|
|
}
|
|
}
|
|
|
|
return workforceNames;
|
|
}
|
|
|
|
/// Récupère les détails des options
|
|
/// Si optionNames est fourni, utilise les noms déjà résolus
|
|
static Future<List<Map<String, dynamic>>> _getOptionsDetails(
|
|
List<Map<String, dynamic>> options, {
|
|
Map<String, String>? optionNames,
|
|
}) async {
|
|
final List<Map<String, dynamic>> optionsWithNames = [];
|
|
|
|
for (final option in options) {
|
|
try {
|
|
String optionName = option['name'] ?? 'Option inconnue';
|
|
|
|
// Si on a l'ID de l'option et les noms résolus, utiliser le nom résolu
|
|
final optionId = option['id'] ?? option['optionId'];
|
|
if (optionId != null && optionNames != null && optionNames.containsKey(optionId)) {
|
|
optionName = optionNames[optionId]!;
|
|
} else if (optionName == 'Option inconnue' && optionId != null) {
|
|
optionName = 'Option $optionId';
|
|
}
|
|
|
|
optionsWithNames.add({
|
|
'name': optionName,
|
|
'quantity': option['quantity'],
|
|
'price': option['price'],
|
|
});
|
|
} 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énéré par EM2 Hub ${AppVersion.fullVersion} http://app.em2events.fr');
|
|
|
|
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';
|
|
}
|
|
}
|
|
|