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
This commit is contained in:
dahoud
2026-01-10 10:43:17 +00:00
parent 06031b01f2
commit 92612abbd7
321 changed files with 43137 additions and 4285 deletions

View File

@@ -1,22 +1,44 @@
class EventModel {
final String id;
final String title;
final String description;
final String startDate;
final String location;
final String category;
final String link;
final String? imageUrl;
final String creatorEmail;
final String creatorFirstName; // Prénom du créateur
final String creatorLastName; // Nom du créateur
final String profileImageUrl;
final List<dynamic> participants;
String status;
final int reactionsCount;
final int commentsCount;
final int sharesCount;
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,
@@ -25,7 +47,6 @@ class EventModel {
required this.location,
required this.category,
required this.link,
this.imageUrl,
required this.creatorEmail,
required this.creatorFirstName,
required this.creatorLastName,
@@ -35,73 +56,211 @@ class EventModel {
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) {
print('[LOG] Création de l\'EventModel depuis 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',
);
}
// Utiliser les valeurs par défaut si une clé est absente
final String id = json['id'] ?? 'ID Inconnu';
final String title = json['title'] ?? 'Titre Inconnu';
final String description = json['description'] ?? 'Description Inconnue';
final String startDate = json['startDate'] ?? 'Date de début Inconnue';
final String location = json['location'] ?? 'Localisation Inconnue';
final String category = json['category'] ?? 'Catégorie Inconnue';
final String link = json['link'] ?? 'Lien Inconnu';
final String? imageUrl = json['imageUrl'];
final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu';
final String creatorFirstName = json['creatorFirstName']; // Ajout du prénom
final String creatorLastName = json['creatorLastName']; // Ajout du nom
final String profileImageUrl = json['profileImageUrl']; // Ajout du nom
final List<dynamic> participants = json['participants'] ?? [];
String status = json['status'] ?? 'ouvert';
final int reactionsCount = json['reactionsCount'] ?? 0;
final int commentsCount = json['commentsCount'] ?? 0;
final int sharesCount = json['sharesCount'] ?? 0;
if (json['title'] == null || json['title'].toString().isEmpty) {
throw ValidationException(
'Le titre de l\'événement est requis',
field: 'title',
);
}
print('[LOG] Champs extraits depuis JSON :');
print(' - ID: $id');
print(' - Titre: $title');
print(' - Description: $description');
print(' - Date de début: $startDate');
print(' - Localisation: $location');
print(' - Catégorie: $category');
print(' - Lien: $link');
print(' - URL de l\'image: ${imageUrl ?? "Aucune"}');
print(' - Email du créateur: $creatorEmail');
print(' - Prénom du créateur: $creatorFirstName');
print(' - Nom du créateur: $creatorLastName');
print(' - Image de profile du créateur: $profileImageUrl');
print(' - Participants: ${participants.length} participants');
print(' - Statut: $status');
print(' - Nombre de réactions: $reactionsCount');
print(' - Nombre de commentaires: $commentsCount');
print(' - Nombre de partages: $sharesCount');
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: id,
title: title,
description: description,
startDate: startDate,
location: location,
category: category,
link: link,
imageUrl: imageUrl,
creatorEmail: creatorEmail,
creatorFirstName: creatorFirstName, // Ajout du prénom
creatorLastName: creatorLastName, // Ajout du nom
profileImageUrl: profileImageUrl,
participants: participants,
status: status,
reactionsCount: reactionsCount,
commentsCount: commentsCount,
sharesCount: sharesCount,
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() {
print('[LOG] Conversion de EventModel en JSON');
return {
final json = <String, dynamic>{
'id': id,
'title': title,
'description': description,
@@ -109,16 +268,166 @@ class EventModel {
'location': location,
'category': category,
'link': link,
'imageUrl': imageUrl,
if (imageUrl != null && imageUrl!.isNotEmpty) 'imageUrl': imageUrl,
'creatorEmail': creatorEmail,
'creatorFirstName': creatorFirstName, // Ajout du prénom
'creatorLastName': creatorLastName, // Ajout du nom
'profileImageUrl': profileImageUrl,
'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;
}