## 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
434 lines
13 KiB
Dart
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;
|
|
}
|