import '../../core/constants/env_config.dart'; import '../../core/errors/exceptions.dart'; import '../../core/utils/app_logger.dart'; import '../../domain/entities/event.dart'; /// Modèle de données pour les événements (Data Transfer Object). /// /// Cette classe est responsable de la sérialisation/désérialisation /// avec l'API backend et convertit vers/depuis l'entité de domaine [Event]. /// /// **Usage:** /// ```dart /// // Depuis JSON /// final event = EventModel.fromJson(jsonData); /// /// // Vers JSON /// final json = event.toJson(); /// /// // Vers entité de domaine /// final entity = event.toEntity(); /// ``` class EventModel { /// Crée une nouvelle instance de [EventModel]. /// /// [id] L'identifiant unique de l'événement /// [title] Le titre de l'événement /// [description] La description de l'événement /// [startDate] La date de début (format ISO 8601 string) /// [location] Le lieu de l'événement /// [category] La catégorie de l'événement /// [link] Le lien associé (optionnel) /// [imageUrl] L'URL de l'image (optionnel) /// [creatorEmail] L'email du créateur /// [creatorFirstName] Le prénom du créateur /// [creatorLastName] Le nom du créateur /// [profileImageUrl] L'URL de l'image de profil du créateur /// [participants] La liste des participants (IDs ou objets) /// [status] Le statut de l'événement ('ouvert', 'fermé', 'annulé') /// [reactionsCount] Le nombre de réactions /// [commentsCount] Le nombre de commentaires /// [sharesCount] Le nombre de partages EventModel({ required this.id, required this.title, required this.description, required this.startDate, required this.location, required this.category, required this.link, required this.creatorEmail, required this.creatorFirstName, required this.creatorLastName, required this.profileImageUrl, required this.participants, required this.status, required this.reactionsCount, required this.commentsCount, required this.sharesCount, this.imageUrl, }); /// L'identifiant unique de l'événement final String id; /// Le titre de l'événement final String title; /// La description de l'événement final String description; /// La date de début (format ISO 8601 string) final String startDate; /// Le lieu de l'événement final String location; /// La catégorie de l'événement final String category; /// Le lien associé à l'événement final String link; /// L'URL de l'image de l'événement (optionnel) final String? imageUrl; /// L'email du créateur de l'événement final String creatorEmail; /// Le prénom du créateur final String creatorFirstName; /// Le nom du créateur final String creatorLastName; /// L'URL de l'image de profil du créateur final String profileImageUrl; /// La liste des participants (peut contenir des IDs ou des objets) final List participants; /// Le statut de l'événement ('ouvert', 'fermé', 'annulé') String status; /// Le nombre de réactions final int reactionsCount; /// Le nombre de commentaires final int commentsCount; /// Le nombre de partages final int sharesCount; // ============================================================================ // FACTORY METHODS // ============================================================================ /// Crée un [EventModel] à partir d'un JSON reçu depuis l'API. /// /// [json] Les données JSON à parser /// /// Returns un [EventModel] avec les données parsées /// /// Throws [ValidationException] si les données essentielles sont manquantes /// /// **Exemple:** /// ```dart /// final json = { /// 'id': '123', /// 'title': 'Concert', /// 'startDate': '2026-01-10T20:00:00Z', /// ... /// }; /// final event = EventModel.fromJson(json); /// ``` factory EventModel.fromJson(Map json) { try { // Validation des champs essentiels if (json['id'] == null || json['id'].toString().isEmpty) { throw ValidationException( 'L\'ID de l\'événement est requis', field: 'id', ); } if (json['title'] == null || json['title'].toString().isEmpty) { throw ValidationException( 'Le titre de l\'événement est requis', field: 'title', ); } if (json['startDate'] == null || json['startDate'].toString().isEmpty) { throw ValidationException( 'La date de début est requise', field: 'startDate', ); } // Parsing avec valeurs par défaut pour les champs optionnels final model = EventModel( id: json['id'].toString(), title: json['title'].toString(), description: json['description']?.toString() ?? '', startDate: json['startDate'].toString(), location: json['location']?.toString() ?? '', category: json['category']?.toString() ?? 'Autre', link: json['link']?.toString() ?? '', imageUrl: json['imageUrl']?.toString(), creatorEmail: json['creatorEmail']?.toString() ?? '', creatorFirstName: json['creatorFirstName']?.toString() ?? '', creatorLastName: json['creatorLastName']?.toString() ?? '', profileImageUrl: json['profileImageUrl']?.toString() ?? '', participants: json['participants'] is List ? json['participants'] as List : [], status: json['status']?.toString() ?? 'ouvert', reactionsCount: _parseInt(json, 'reactionsCount') ?? 0, commentsCount: _parseInt(json, 'commentsCount') ?? 0, sharesCount: _parseInt(json, 'sharesCount') ?? 0, ); if (EnvConfig.enableDetailedLogs) { _logEventParsed(model); } return model; } catch (e, stackTrace) { if (e is ValidationException) rethrow; AppLogger.e('Erreur lors du parsing JSON', error: e, stackTrace: stackTrace, tag: 'EventModel'); throw ValidationException( 'Erreur lors du parsing de l\'événement: $e', ); } } /// Parse une valeur int depuis le JSON. static int? _parseInt(Map json, String key) { final value = json[key]; if (value == null) return null; if (value is int) return value; if (value is String) { return int.tryParse(value); } if (value is double) { return value.toInt(); } return null; } /// Log les détails d'un événement parsé (uniquement en mode debug). static void _logEventParsed(EventModel event) { AppLogger.d('Événement parsé: ID=${event.id}, Titre=${event.title}, Date=${event.startDate}, Localisation=${event.location}, Statut=${event.status}, Participants=${event.participants.length}', tag: 'EventModel'); } /// Crée un [EventModel] depuis une entité de domaine [Event]. /// /// [event] L'entité de domaine à convertir /// /// Returns un [EventModel] avec les données de l'entité /// /// **Exemple:** /// ```dart /// final entity = Event(...); /// final model = EventModel.fromEntity(entity); /// ``` factory EventModel.fromEntity(Event event) { return EventModel( id: event.id, title: event.title, description: event.description, startDate: event.startDate.toIso8601String(), location: event.location, category: event.category, link: event.link ?? '', imageUrl: event.imageUrl, creatorEmail: event.creatorEmail, creatorFirstName: event.creatorFirstName, creatorLastName: event.creatorLastName, profileImageUrl: event.creatorProfileImageUrl, participants: event.participantIds, status: event.status.toApiString(), reactionsCount: event.reactionsCount, commentsCount: event.commentsCount, sharesCount: event.sharesCount, ); } // ============================================================================ // CONVERSION METHODS // ============================================================================ /// Convertit ce [EventModel] en JSON pour l'envoi vers l'API. /// /// Returns une [Map] contenant les données de l'événement /// /// **Exemple:** /// ```dart /// final event = EventModel(...); /// final json = event.toJson(); /// // Envoyer json à l'API /// ``` Map toJson() { final json = { 'id': id, 'title': title, 'description': description, 'startDate': startDate, 'location': location, 'category': category, 'link': link, if (imageUrl != null && imageUrl!.isNotEmpty) 'imageUrl': imageUrl, 'creatorEmail': creatorEmail, 'creatorFirstName': creatorFirstName, 'creatorLastName': creatorLastName, if (profileImageUrl.isNotEmpty) 'profileImageUrl': profileImageUrl, 'participants': participants, 'status': status, 'reactionsCount': reactionsCount, 'commentsCount': commentsCount, 'sharesCount': sharesCount, }; AppLogger.d('Conversion en JSON pour l\'événement: $id', tag: 'EventModel'); return json; } /// Convertit ce modèle vers une entité de domaine [Event]. /// /// Returns une instance de [Event] avec les mêmes données /// /// Throws [ValidationException] si la date ne peut pas être parsée /// /// **Exemple:** /// ```dart /// final model = EventModel.fromJson(json); /// final entity = model.toEntity(); /// ``` Event toEntity() { DateTime parsedDate; try { parsedDate = DateTime.parse(startDate); } catch (e) { throw ValidationException( 'Format de date invalide: $startDate', field: 'startDate', ); } // Convertir les participants en liste de strings final participantIds = participants.map((p) { if (p is Map) { return p['id']?.toString() ?? p['userId']?.toString() ?? ''; } return p.toString(); }).where((id) => id.isNotEmpty).toList(); return Event( id: id, title: title, description: description, startDate: parsedDate, location: location, category: category, link: link.isEmpty ? null : link, imageUrl: imageUrl, creatorEmail: creatorEmail, creatorFirstName: creatorFirstName, creatorLastName: creatorLastName, creatorProfileImageUrl: profileImageUrl, participantIds: participantIds, status: EventStatus.fromString(status), reactionsCount: reactionsCount, commentsCount: commentsCount, sharesCount: sharesCount, ); } // ============================================================================ // UTILITY METHODS // ============================================================================ /// Crée une copie de ce [EventModel] avec des valeurs modifiées. /// /// Tous les paramètres sont optionnels. Seuls les paramètres fournis /// seront modifiés dans la nouvelle instance. /// /// **Exemple:** /// ```dart /// final updated = event.copyWith( /// title: 'Nouveau titre', /// status: 'fermé', /// ); /// ``` EventModel copyWith({ String? id, String? title, String? description, String? startDate, String? location, String? category, String? link, String? imageUrl, String? creatorEmail, String? creatorFirstName, String? creatorLastName, String? profileImageUrl, List? participants, String? status, int? reactionsCount, int? commentsCount, int? sharesCount, }) { return EventModel( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, startDate: startDate ?? this.startDate, location: location ?? this.location, category: category ?? this.category, link: link ?? this.link, imageUrl: imageUrl ?? this.imageUrl, creatorEmail: creatorEmail ?? this.creatorEmail, creatorFirstName: creatorFirstName ?? this.creatorFirstName, creatorLastName: creatorLastName ?? this.creatorLastName, profileImageUrl: profileImageUrl ?? this.profileImageUrl, participants: participants ?? this.participants, status: status ?? this.status, reactionsCount: reactionsCount ?? this.reactionsCount, commentsCount: commentsCount ?? this.commentsCount, sharesCount: sharesCount ?? this.sharesCount, ); } /// Retourne le nombre de participants. /// /// Returns le nombre de participants dans la liste int get participantsCount => participants.length; /// Vérifie si l'événement est ouvert. /// /// Returns `true` si le statut est 'ouvert', `false` sinon bool get isOpen => status.toLowerCase() == 'ouvert'; /// Vérifie si l'événement est fermé. /// /// Returns `true` si le statut est 'fermé', `false` sinon bool get isClosed => status.toLowerCase() == 'fermé'; /// Vérifie si l'événement est annulé. /// /// Returns `true` si le statut est 'annulé', `false` sinon bool get isCancelled => status.toLowerCase() == 'annulé'; @override String toString() { return 'EventModel(' 'id: $id, ' 'title: $title, ' 'startDate: $startDate, ' 'status: $status' ')'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is EventModel && other.id == id; } @override int get hashCode => id.hashCode; }