Files
EM2_ERP/em2rp/lib/services/ics_export_service.dart
ElPoyo b30ae0f10a feat: Sécurisation Firestore, gestion des prix HT/TTC et refactorisation majeure
Cette mise à jour verrouille l'accès direct à Firestore depuis le client pour renforcer la sécurité et introduit une gestion complète des prix HT/TTC dans toute l'application. Elle apporte également des améliorations significatives des permissions, des optimisations de performance et de nouvelles fonctionnalités.

### Sécurité et Backend
- **Firestore Rules :** Ajout de `firestore.rules` qui bloque par défaut tous les accès en lecture/écriture depuis le client. Toutes les opérations de données doivent maintenant passer par les Cloud Functions, renforçant considérablement la sécurité.
- **Index Firestore :** Création d'un fichier `firestore.indexes.json` pour optimiser les requêtes sur la collection `events`.
- **Cloud Functions :** Les fonctions de création/mise à jour d'événements ont été adaptées pour accepter des ID de documents (utilisateurs, type d'événement) et les convertir en `DocumentReference` côté serveur, simplifiant les appels depuis le client.

### Gestion des Prix HT/TTC
- **Calcul Automatisé :** Introduction d'un helper `PriceHelpers` et d'un widget `PriceHtTtcFields` pour calculer et synchroniser automatiquement les prix HT et TTC dans le formulaire d'événement.
- **Affichage Détaillé :**
    - Les détails des événements et des options affichent désormais les prix HT, la TVA et le TTC séparément pour plus de clarté.
    - Le prix de base (`basePrice`) est maintenant traité comme un prix TTC dans toute l'application.

### Permissions et Rôles
- **Centralisation (`AppPermission`) :** Création d'une énumération `AppPermission` pour centraliser toutes les permissions de l'application, avec descriptions et catégories.
- **Rôles Prédéfinis :** Définition de rôles standards (Admin, Manager, Technicien, User) avec des jeux de permissions prédéfinis.
- **Filtre par Utilisateur :** Ajout d'un filtre par utilisateur sur la page Calendrier, visible uniquement pour les utilisateurs ayant la permission `view_all_user_events`.

### Améliorations et Optimisations (Frontend)
- **`DebugLog` :** Ajout d'un utilitaire `DebugLog` pour gérer les logs, qui sont automatiquement désactivés en mode production.
- **Optimisation du Sélecteur d'Équipement :**
    - La boîte de dialogue de sélection d'équipement a été lourdement optimisée pour éviter les reconstructions complètes de la liste lors de la sélection/désélection d'items.
    - Utilisation de `ValueNotifier` et de caches locaux (`_cachedContainers`, `_cachedEquipment`) pour des mises à jour d'UI plus ciblées et fluides.
    - La position du scroll est désormais préservée.
- **Catégorie d'Équipement :** Ajout de la catégorie `Vehicle` (Véhicule) pour les équipements.
- **Formulaires :** Les formulaires de création/modification d'événements et d'équipements ont été nettoyés de leurs logs de débogage excessifs.
2026-01-14 17:32:58 +01:00

244 lines
7.6 KiB
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
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';
}
}