feat: Refonte de la checklist de préparation avec gestion des manquants et des containers

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.
This commit is contained in:
ElPoyo
2026-01-15 12:05:37 +01:00
parent b30ae0f10a
commit 60d0e1c6c4
10 changed files with 885 additions and 336 deletions

View File

@@ -1,3 +1,4 @@
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';
@@ -5,21 +6,30 @@ 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 {
///
/// [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 eventTypeName = await _getEventTypeName(event.eventTypeId);
final workforce = await _getWorkforceDetails(event.workforce);
final optionsWithNames = await _getOptionsDetails(event.options);
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, eventTypeName, workforce, optionsWithNames);
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';
@@ -39,7 +49,7 @@ SUMMARY:${_escapeIcsText(event.name)}
DESCRIPTION:${_escapeIcsText(description)}
LOCATION:${_escapeIcsText(event.address)}
STATUS:${_getEventStatus(event.status)}
CATEGORIES:${_escapeIcsText(eventTypeName)}
CATEGORIES:${_escapeIcsText(resolvedEventTypeName)}
END:VEVENT
END:VCALENDAR''';
@@ -57,9 +67,11 @@ END:VCALENDAR''';
}
/// 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 {
/// 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) {
@@ -74,16 +86,24 @@ END:VCALENDAR''';
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
// Si c'est un String (UID) et qu'on a les noms résolus, les utiliser
if (ref is String) {
workforceNames.add('Utilisateur $ref');
if (userNames != null && userNames.containsKey(ref)) {
workforceNames.add(userNames[ref]!);
} else {
workforceNames.add('Utilisateur $ref');
}
continue;
}
// Si c'est une DocumentReference, extraire l'ID seulement
// Si c'est une DocumentReference
if (ref is DocumentReference) {
workforceNames.add('Utilisateur ${ref.id}');
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');
@@ -94,16 +114,29 @@ END:VCALENDAR''';
}
/// 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 {
/// 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 {
// Les options devraient déjà contenir le nom
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': option['name'] ?? option['optionId'] ?? 'Option inconnue',
'name': optionName,
'quantity': option['quantity'],
'price': option['price'],
});
} catch (e) {
print('Erreur lors du traitement des options: $e');
@@ -198,7 +231,7 @@ END:VCALENDAR''';
// Lien vers l'application
buffer.writeln('');
buffer.writeln('---');
buffer.writeln('Géré par EM2RP Event Manager');
buffer.writeln('ré par EM2 ERP ${AppVersion.fullVersion} http://app.em2events.fr');
return buffer.toString();
}