Files
afterwork/lib/data/models/event_model.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
2026-01-10 10:43:17 +00:00

434 lines
13 KiB
Dart

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<dynamic> 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<String, dynamic> 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<dynamic>
: [],
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<String, dynamic> 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<String, dynamic> toJson() {
final json = <String, dynamic>{
'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<dynamic>? 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;
}