import 'package:em2rp/config/app_version.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:intl/intl.dart'; import 'package:em2rp/utils/debug_log.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 generateIcsContent( EventModel event, { String? eventTypeName, Map? userNames, Map? 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 _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> _getWorkforceDetails( List workforce, { Map? userNames, }) async { final List 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) { 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>> _getOptionsDetails( List> options, { Map? optionNames, }) async { final List> 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 workforce, List> 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 ERP ${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'; } }