Cette mise à jour refond entièrement l'interface et la logique de la checklist de préparation d'événement. Elle introduit la notion d'équipements "manquants", une gestion visuelle des containers et de leur contenu, et une logique plus fine pour le suivi des quantités et des statuts à chaque étape.
**Features et Améliorations :**
- **Gestion des Équipements Manquants :**
- Le modèle `EventEquipment` a été enrichi pour tracer si un équipement est manquant à chaque étape (`isMissingAtPreparation`, `isMissingAtLoading`, etc.).
- Un équipement non validé lors de la confirmation d'une étape est désormais marqué comme "manquant" pour les étapes suivantes.
- Les équipements qui étaient manquants à l'étape précédente sont maintenant visuellement mis en évidence avec une bordure et une icône orange, et une confirmation est demandée pour les valider.
- **Refonte de la Checklist (UI/UX) :**
- **Groupement par Container :** La checklist affiche désormais les containers comme des en-têtes de groupe. Les équipements qu'ils contiennent sont listés en dessous, avec une indentation visuelle.
- **Validation Groupée :** Il est possible de valider tous les équipements d'un container en un seul clic sur l'en-tête du container.
- **Nouveau Widget `ContainerChecklistItem` :** Créé pour afficher un container et ses équipements enfants dans la checklist.
- **Refonte de `EquipmentChecklistItem` :** Le widget a été entièrement revu pour un design plus clair, une meilleure gestion des états (validé, manquant), et un affichage compact pour les équipements enfants.
- **Logique de Suivi Améliorée :**
- **Quantités par Étape :** Le modèle `EventEquipment` et l'interface de préparation permettent maintenant de suivre les quantités réelles à chaque étape (`quantityAtPreparation`, `quantityAtLoading`, etc.), au lieu d'une seule quantité de retour.
- **Marquage Automatique des "Perdus" :** À l'étape finale du retour, un équipement qui était présent au départ mais qui est maintenant manquant sera automatiquement marqué avec le statut "lost" dans la base de données.
- **Flux de Validation :** Le processus de confirmation distingue désormais la validation de tous les équipements et la confirmation de l'état actuel (y compris les manquants).
- **Export ICS Enrichi :**
- L'export ICS inclut désormais les noms résolus des utilisateurs (main d'œuvre) pour plus de clarté, en plus des détails de l'événement.
- Le contenu généré mentionne la version de l'application.
277 lines
8.8 KiB
Dart
277 lines
8.8 KiB
Dart
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<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 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';
|
|
}
|
|
}
|
|
|