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

@@ -0,0 +1,136 @@
import '../../domain/entities/chat_message.dart';
/// Modèle de données pour les messages de chat (Data Transfer Object).
class ChatMessageModel {
ChatMessageModel({
required this.id,
required this.conversationId,
required this.senderId,
required this.senderFirstName,
required this.senderLastName,
this.senderProfileImageUrl,
required this.content,
required this.timestamp,
required this.isRead,
this.isDelivered = false,
this.attachmentUrl,
this.attachmentType,
});
/// Factory pour créer un [ChatMessageModel] à partir d'un JSON.
factory ChatMessageModel.fromJson(Map<String, dynamic> json) {
return ChatMessageModel(
id: _parseId(json, 'id', ''),
conversationId: _parseString(json, 'conversationId', ''),
senderId: _parseId(json, 'senderId', ''),
senderFirstName: _parseString(json, 'senderFirstName', ''),
senderLastName: _parseString(json, 'senderLastName', ''),
senderProfileImageUrl: json['senderProfileImageUrl'] as String?,
content: _parseString(json, 'content', ''),
timestamp: DateTime.parse(json['timestamp'] as String),
isRead: json['isRead'] as bool? ?? false,
isDelivered: json['isDelivered'] as bool? ?? false,
attachmentUrl: json['attachmentUrl'] as String?,
attachmentType: _parseAttachmentType(json['attachmentType'] as String?),
);
}
/// Factory pour créer un [ChatMessageModel] à partir d'une entité.
factory ChatMessageModel.fromEntity(ChatMessage message) {
return ChatMessageModel(
id: message.id,
conversationId: message.conversationId,
senderId: message.senderId,
senderFirstName: message.senderFirstName,
senderLastName: message.senderLastName,
senderProfileImageUrl: message.senderProfileImageUrl,
content: message.content,
timestamp: message.timestamp,
isRead: message.isRead,
isDelivered: message.isDelivered,
attachmentUrl: message.attachmentUrl,
attachmentType: message.attachmentType,
);
}
final String id;
final String conversationId;
final String senderId;
final String senderFirstName;
final String senderLastName;
final String? senderProfileImageUrl;
final String content;
final DateTime timestamp;
final bool isRead;
final bool isDelivered;
final String? attachmentUrl;
final AttachmentType? attachmentType;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'conversationId': conversationId,
'senderId': senderId,
'senderFirstName': senderFirstName,
'senderLastName': senderLastName,
if (senderProfileImageUrl != null) 'senderProfileImageUrl': senderProfileImageUrl,
'content': content,
'timestamp': timestamp.toIso8601String(),
'isRead': isRead,
'isDelivered': isDelivered,
if (attachmentUrl != null) 'attachmentUrl': attachmentUrl,
if (attachmentType != null) 'attachmentType': _attachmentTypeToString(attachmentType!),
};
}
/// Convertit ce modèle vers une entité de domaine [ChatMessage].
ChatMessage toEntity() {
return ChatMessage(
id: id,
conversationId: conversationId,
senderId: senderId,
senderFirstName: senderFirstName,
senderLastName: senderLastName,
senderProfileImageUrl: senderProfileImageUrl,
content: content,
timestamp: timestamp,
isRead: isRead,
isDelivered: isDelivered,
attachmentUrl: attachmentUrl,
attachmentType: attachmentType,
);
}
// Méthodes de parsing
static String _parseString(Map<String, dynamic> json, String key, String defaultValue) {
return json[key] as String? ?? defaultValue;
}
static String _parseId(Map<String, dynamic> json, String key, String defaultValue) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
static AttachmentType? _parseAttachmentType(String? type) {
if (type == null) return null;
switch (type.toLowerCase()) {
case 'image':
return AttachmentType.image;
case 'video':
return AttachmentType.video;
case 'audio':
return AttachmentType.audio;
case 'file':
return AttachmentType.file;
default:
return null;
}
}
static String _attachmentTypeToString(AttachmentType type) {
return type.toString().split('.').last;
}
}

View File

@@ -0,0 +1,128 @@
import '../../domain/entities/comment.dart';
/// Modèle de données pour les commentaires (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 Comment.
class CommentModel {
CommentModel({
required this.id,
required this.postId,
required this.userId,
required this.userFirstName,
required this.userLastName,
required this.userProfileImageUrl,
required this.content,
required this.timestamp,
});
/// Factory pour créer un [CommentModel] à partir d'un JSON.
factory CommentModel.fromJson(Map<String, dynamic> json) {
return CommentModel(
id: _parseId(json, 'id', ''),
postId: _parseId(json, 'postId', ''),
userId: _parseId(json, 'userId', ''),
userFirstName: _parseString(json, 'userFirstName', ''),
userLastName: _parseString(json, 'userLastName', ''),
userProfileImageUrl: _parseString(json, 'userProfileImageUrl', ''),
content: _parseString(json, 'content', ''),
timestamp: _parseTimestamp(json['timestamp']),
);
}
/// Crée un [CommentModel] depuis une entité de domaine [Comment].
factory CommentModel.fromEntity(Comment comment) {
return CommentModel(
id: comment.id,
postId: comment.postId,
userId: comment.userId,
userFirstName: comment.userFirstName,
userLastName: comment.userLastName,
userProfileImageUrl: comment.userProfileImageUrl,
content: comment.content,
timestamp: comment.timestamp,
);
}
final String id;
final String postId;
final String userId;
final String userFirstName;
final String userLastName;
final String userProfileImageUrl;
final String content;
final DateTime timestamp;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'postId': postId,
'userId': userId,
'userFirstName': userFirstName,
'userLastName': userLastName,
'userProfileImageUrl': userProfileImageUrl,
'content': content,
'timestamp': timestamp.toIso8601String(),
};
}
/// Convertit ce modèle vers une entité de domaine [Comment].
Comment toEntity() {
return Comment(
id: id,
postId: postId,
userId: userId,
userFirstName: userFirstName,
userLastName: userLastName,
userProfileImageUrl: userProfileImageUrl,
content: content,
timestamp: timestamp,
);
}
/// Parse une valeur string depuis le JSON avec valeur par défaut.
static String _parseString(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
return json[key] as String? ?? defaultValue;
}
/// Parse un timestamp depuis le JSON.
static DateTime _parseTimestamp(dynamic timestamp) {
if (timestamp == null) return DateTime.now();
if (timestamp is String) {
try {
return DateTime.parse(timestamp);
} catch (e) {
return DateTime.now();
}
}
if (timestamp is int) {
return DateTime.fromMillisecondsSinceEpoch(timestamp);
}
return DateTime.now();
}
/// Parse un ID (UUID) depuis le JSON.
///
/// [json] Le JSON à parser
/// [key] La clé de l'ID
/// [defaultValue] La valeur par défaut si l'ID est null
///
/// Returns l'ID parsé ou la valeur par défaut
static String _parseId(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
}

View File

@@ -0,0 +1,99 @@
import '../../domain/entities/conversation.dart';
/// Modèle de données pour les conversations (Data Transfer Object).
class ConversationModel {
ConversationModel({
required this.id,
required this.participantId,
required this.participantFirstName,
required this.participantLastName,
this.participantProfileImageUrl,
this.lastMessage,
this.lastMessageTimestamp,
required this.unreadCount,
this.isTyping = false,
});
/// Factory pour créer un [ConversationModel] à partir d'un JSON.
factory ConversationModel.fromJson(Map<String, dynamic> json) {
return ConversationModel(
id: _parseId(json, 'id', ''),
participantId: _parseId(json, 'participantId', ''),
participantFirstName: _parseString(json, 'participantFirstName', ''),
participantLastName: _parseString(json, 'participantLastName', ''),
participantProfileImageUrl: json['participantProfileImageUrl'] as String?,
lastMessage: json['lastMessage'] as String?,
lastMessageTimestamp: json['lastMessageTimestamp'] != null
? DateTime.parse(json['lastMessageTimestamp'] as String)
: null,
unreadCount: json['unreadCount'] as int? ?? 0,
isTyping: json['isTyping'] as bool? ?? false,
);
}
/// Factory pour créer un [ConversationModel] à partir d'une entité.
factory ConversationModel.fromEntity(Conversation conversation) {
return ConversationModel(
id: conversation.id,
participantId: conversation.participantId,
participantFirstName: conversation.participantFirstName,
participantLastName: conversation.participantLastName,
participantProfileImageUrl: conversation.participantProfileImageUrl,
lastMessage: conversation.lastMessage,
lastMessageTimestamp: conversation.lastMessageTimestamp,
unreadCount: conversation.unreadCount,
isTyping: conversation.isTyping,
);
}
final String id;
final String participantId;
final String participantFirstName;
final String participantLastName;
final String? participantProfileImageUrl;
final String? lastMessage;
final DateTime? lastMessageTimestamp;
final int unreadCount;
final bool isTyping;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'participantId': participantId,
'participantFirstName': participantFirstName,
'participantLastName': participantLastName,
if (participantProfileImageUrl != null) 'participantProfileImageUrl': participantProfileImageUrl,
if (lastMessage != null) 'lastMessage': lastMessage,
if (lastMessageTimestamp != null) 'lastMessageTimestamp': lastMessageTimestamp!.toIso8601String(),
'unreadCount': unreadCount,
'isTyping': isTyping,
};
}
/// Convertit ce modèle vers une entité de domaine [Conversation].
Conversation toEntity() {
return Conversation(
id: id,
participantId: participantId,
participantFirstName: participantFirstName,
participantLastName: participantLastName,
participantProfileImageUrl: participantProfileImageUrl,
lastMessage: lastMessage,
lastMessageTimestamp: lastMessageTimestamp,
unreadCount: unreadCount,
isTyping: isTyping,
);
}
// Méthodes de parsing
static String _parseString(Map<String, dynamic> json, String key, String defaultValue) {
return json[key] as String? ?? defaultValue;
}
static String _parseId(Map<String, dynamic> json, String key, String defaultValue) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
}

View File

@@ -1,8 +1,8 @@
import 'package:afterwork/data/models/user_model.dart';
import 'user_model.dart';
/// Modèle représentant le créateur d'un événement.
class CreatorModel extends UserModel {
CreatorModel({
const CreatorModel({
required String id,
required String nom,
required String prenoms,

View File

@@ -0,0 +1,190 @@
import '../../domain/entities/establishment.dart';
/// Modèle de données pour les établissements (Data Transfer Object).
class EstablishmentModel {
EstablishmentModel({
required this.id,
required this.name,
required this.type,
required this.address,
required this.city,
required this.postalCode,
this.description,
this.phoneNumber,
this.email,
this.website,
this.imageUrl,
this.rating,
this.priceRange,
this.capacity,
this.amenities = const [],
this.openingHours,
this.latitude,
this.longitude,
});
/// Factory pour créer un [EstablishmentModel] à partir d'un JSON.
factory EstablishmentModel.fromJson(Map<String, dynamic> json) {
return EstablishmentModel(
id: _parseId(json, 'id', ''),
name: _parseString(json, 'name', ''),
type: _parseType(json['type'] as String?),
address: _parseString(json, 'address', ''),
city: _parseString(json, 'city', ''),
postalCode: _parseString(json, 'postalCode', ''),
description: json['description'] as String?,
phoneNumber: json['phoneNumber'] as String?,
email: json['email'] as String?,
website: json['website'] as String?,
imageUrl: json['imageUrl'] as String?,
rating: json['rating'] != null ? (json['rating'] as num).toDouble() : null,
priceRange: _parsePriceRange(json['priceRange'] as String?),
capacity: json['capacity'] as int?,
amenities: json['amenities'] != null
? List<String>.from(json['amenities'] as List)
: [],
openingHours: json['openingHours'] as String?,
latitude: json['latitude'] != null ? (json['latitude'] as num).toDouble() : null,
longitude: json['longitude'] != null ? (json['longitude'] as num).toDouble() : null,
);
}
final String id;
final String name;
final EstablishmentType type;
final String address;
final String city;
final String postalCode;
final String? description;
final String? phoneNumber;
final String? email;
final String? website;
final String? imageUrl;
final double? rating;
final PriceRange? priceRange;
final int? capacity;
final List<String> amenities;
final String? openingHours;
final double? latitude;
final double? longitude;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'type': _typeToString(type),
'address': address,
'city': city,
'postalCode': postalCode,
if (description != null) 'description': description,
if (phoneNumber != null) 'phoneNumber': phoneNumber,
if (email != null) 'email': email,
if (website != null) 'website': website,
if (imageUrl != null) 'imageUrl': imageUrl,
if (rating != null) 'rating': rating,
if (priceRange != null) 'priceRange': _priceRangeToString(priceRange!),
if (capacity != null) 'capacity': capacity,
if (amenities.isNotEmpty) 'amenities': amenities,
if (openingHours != null) 'openingHours': openingHours,
if (latitude != null) 'latitude': latitude,
if (longitude != null) 'longitude': longitude,
};
}
/// Convertit ce modèle vers une entité de domaine [Establishment].
Establishment toEntity() {
return Establishment(
id: id,
name: name,
type: type,
address: address,
city: city,
postalCode: postalCode,
description: description,
phoneNumber: phoneNumber,
email: email,
website: website,
imageUrl: imageUrl,
rating: rating,
priceRange: priceRange,
capacity: capacity,
amenities: amenities,
openingHours: openingHours,
latitude: latitude,
longitude: longitude,
);
}
// Méthodes de parsing
static String _parseString(Map<String, dynamic> json, String key, String defaultValue) {
return json[key] as String? ?? defaultValue;
}
static String _parseId(Map<String, dynamic> json, String key, String defaultValue) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
static EstablishmentType _parseType(String? type) {
if (type == null) return EstablishmentType.other;
switch (type.toLowerCase()) {
case 'bar':
return EstablishmentType.bar;
case 'restaurant':
return EstablishmentType.restaurant;
case 'club':
return EstablishmentType.club;
case 'cafe':
case 'café':
return EstablishmentType.cafe;
case 'lounge':
return EstablishmentType.lounge;
case 'pub':
return EstablishmentType.pub;
case 'brewery':
case 'brasserie':
return EstablishmentType.brewery;
case 'winery':
case 'cave':
return EstablishmentType.winery;
default:
return EstablishmentType.other;
}
}
static PriceRange? _parsePriceRange(String? priceRange) {
if (priceRange == null) return null;
switch (priceRange.toLowerCase()) {
case 'cheap':
case 'économique':
case '':
return PriceRange.cheap;
case 'moderate':
case 'modéré':
case '€€':
return PriceRange.moderate;
case 'expensive':
case 'cher':
case '€€€':
return PriceRange.expensive;
case 'luxury':
case 'luxe':
case '€€€€':
return PriceRange.luxury;
default:
return null;
}
}
static String _typeToString(EstablishmentType type) {
return type.toString().split('.').last;
}
static String _priceRangeToString(PriceRange priceRange) {
return priceRange.toString().split('.').last;
}
}

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;
}

View File

@@ -0,0 +1,78 @@
import '../../domain/entities/friend_suggestion.dart';
/// Modèle de données pour une suggestion d'ami.
///
/// Cette classe hérite de [FriendSuggestion] et ajoute les fonctionnalités
/// de conversion depuis/vers JSON pour la communication avec l'API.
class FriendSuggestionModel extends FriendSuggestion {
const FriendSuggestionModel({
required super.userId,
required super.firstName,
required super.lastName,
required super.email,
required super.profileImageUrl,
required super.mutualFriendsCount,
required super.suggestionReason,
});
/// Factory pour créer un [FriendSuggestionModel] depuis un JSON.
///
/// Le backend renvoie :
/// - userId : UUID de l'utilisateur suggéré
/// - prenoms : Prénom(s) de l'utilisateur
/// - nom : Nom de famille de l'utilisateur
/// - email : Adresse email
/// - profileImageUrl : URL de l'image de profil
/// - mutualFriendsCount : Nombre d'amis en commun
/// - suggestionReason : Raison de la suggestion
factory FriendSuggestionModel.fromJson(Map<String, dynamic> json) {
return FriendSuggestionModel(
userId: json['userId']?.toString() ?? '',
firstName: json['prenoms']?.toString() ?? json['firstName']?.toString() ?? '',
lastName: json['nom']?.toString() ?? json['lastName']?.toString() ?? '',
email: json['email']?.toString() ?? '',
profileImageUrl: json['profileImageUrl']?.toString() ?? '',
mutualFriendsCount: (json['mutualFriendsCount'] as num?)?.toInt() ?? 0,
suggestionReason: json['suggestionReason']?.toString() ?? 'Suggestion',
);
}
/// Convertit le modèle en JSON.
Map<String, dynamic> toJson() {
return {
'userId': userId,
'prenoms': firstName,
'nom': lastName,
'email': email,
'profileImageUrl': profileImageUrl,
'mutualFriendsCount': mutualFriendsCount,
'suggestionReason': suggestionReason,
};
}
/// Convertit le modèle en entité de domaine.
FriendSuggestion toEntity() {
return FriendSuggestion(
userId: userId,
firstName: firstName,
lastName: lastName,
email: email,
profileImageUrl: profileImageUrl,
mutualFriendsCount: mutualFriendsCount,
suggestionReason: suggestionReason,
);
}
/// Factory pour créer un [FriendSuggestionModel] depuis une entité.
factory FriendSuggestionModel.fromEntity(FriendSuggestion entity) {
return FriendSuggestionModel(
userId: entity.userId,
firstName: entity.firstName,
lastName: entity.lastName,
email: entity.email,
profileImageUrl: entity.profileImageUrl,
mutualFriendsCount: entity.mutualFriendsCount,
suggestionReason: entity.suggestionReason,
);
}
}

View File

@@ -0,0 +1,181 @@
import '../../domain/entities/notification.dart';
/// Modèle de données pour les notifications (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 Notification.
class NotificationModel {
NotificationModel({
required this.id,
required this.title,
required this.message,
required this.type,
required this.timestamp,
this.isRead = false,
this.eventId,
this.userId,
this.metadata,
});
/// Factory pour créer un [NotificationModel] à partir d'un JSON.
factory NotificationModel.fromJson(Map<String, dynamic> json) {
return NotificationModel(
id: _parseIdRequired(json, 'id', ''),
title: _parseString(json, 'title', 'Notification'),
message: _parseString(json, 'message', ''),
type: _parseNotificationType(json['type'] as String?),
timestamp: _parseTimestamp(json['timestamp']),
isRead: json['isRead'] as bool? ?? false,
eventId: _parseId(json, 'eventId'),
userId: _parseId(json, 'userId'),
metadata: _parseMetadata(json['metadata']),
);
}
/// Crée un [NotificationModel] depuis une entité de domaine [Notification].
factory NotificationModel.fromEntity(Notification notification) {
return NotificationModel(
id: notification.id,
title: notification.title,
message: notification.message,
type: notification.type,
timestamp: notification.timestamp,
isRead: notification.isRead,
eventId: notification.eventId,
userId: notification.userId,
metadata: notification.metadata,
);
}
final String id;
final String title;
final String message;
final NotificationType type;
final DateTime timestamp;
bool isRead;
final String? eventId;
final String? userId;
final Map<String, dynamic>? metadata;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'message': message,
'type': type.toString().split('.').last,
'timestamp': timestamp.toIso8601String(),
'isRead': isRead,
if (eventId != null) 'eventId': eventId,
if (userId != null) 'userId': userId,
if (metadata != null) 'metadata': metadata,
};
}
/// Convertit ce modèle vers une entité de domaine [Notification].
Notification toEntity() {
return Notification(
id: id,
title: title,
message: message,
type: type,
timestamp: timestamp,
isRead: isRead,
eventId: eventId,
userId: userId,
metadata: metadata,
);
}
/// Parse une valeur string depuis le JSON avec valeur par défaut.
static String _parseString(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
return json[key] as String? ?? defaultValue;
}
/// Parse le type de notification depuis le JSON.
static NotificationType _parseNotificationType(String? type) {
if (type == null) return NotificationType.other;
switch (type.toLowerCase()) {
case 'event':
case 'événement':
return NotificationType.event;
case 'friend':
case 'ami':
return NotificationType.friend;
case 'reminder':
case 'rappel':
return NotificationType.reminder;
default:
return NotificationType.other;
}
}
/// Parse un timestamp depuis le JSON.
static DateTime _parseTimestamp(dynamic timestamp) {
if (timestamp == null) return DateTime.now();
if (timestamp is String) {
try {
return DateTime.parse(timestamp);
} catch (e) {
return DateTime.now();
}
}
if (timestamp is int) {
return DateTime.fromMillisecondsSinceEpoch(timestamp);
}
return DateTime.now();
}
/// Parse un ID (UUID) depuis le JSON.
///
/// [json] Le JSON à parser
/// [key] La clé de l'ID
/// [defaultValue] La valeur par défaut si l'ID est null
///
/// Returns l'ID parsé ou la valeur par défaut
static String _parseIdRequired(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
/// Parse un ID (UUID) optionnel depuis le JSON.
///
/// [json] Le JSON à parser
/// [key] La clé de l'ID
///
/// Returns l'ID parsé ou null
static String? _parseId(Map<String, dynamic> json, String key) {
final value = json[key];
if (value == null) return null;
return value.toString();
}
/// Parse les métadonnées depuis le JSON.
static Map<String, dynamic>? _parseMetadata(dynamic metadata) {
if (metadata == null) return null;
if (metadata is Map<String, dynamic>) return metadata;
if (metadata is String) {
try {
// Tenter de parser si c'est une chaîne JSON
return {'raw': metadata};
} catch (e) {
return null;
}
}
return null;
}
}

View File

@@ -1,8 +1,8 @@
import 'package:afterwork/data/models/user_model.dart';
import 'user_model.dart';
/// Modèle représentant un participant à un événement.
class ParticipantModel extends UserModel {
ParticipantModel({
const ParticipantModel({
required String id,
required String nom,
required String prenoms,

View File

@@ -0,0 +1,192 @@
import '../../domain/entities/reservation.dart';
/// Modèle de données pour les réservations (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 Reservation.
class ReservationModel {
ReservationModel({
required this.id,
required this.userId,
required this.userFullName,
required this.eventId,
required this.eventTitle,
required this.reservationDate,
required this.numberOfPeople,
required this.status,
this.establishmentId,
this.establishmentName,
this.notes,
this.createdAt,
});
/// Factory pour créer un [ReservationModel] à partir d'un JSON.
factory ReservationModel.fromJson(Map<String, dynamic> json) {
return ReservationModel(
id: _parseId(json, 'id', ''),
userId: _parseId(json, 'userId', ''),
userFullName: _parseString(json, 'userFullName', ''),
eventId: _parseId(json, 'eventId', ''),
eventTitle: _parseString(json, 'eventTitle', ''),
reservationDate: _parseTimestamp(json['reservationDate']),
numberOfPeople: _parseInt(json, 'numberOfPeople'),
status: _parseStatus(json['status'] as String?),
establishmentId: json['establishmentId'] as String?,
establishmentName: json['establishmentName'] as String?,
notes: json['notes'] as String?,
createdAt: json['createdAt'] != null
? _parseTimestamp(json['createdAt'])
: null,
);
}
/// Crée un [ReservationModel] depuis une entité de domaine [Reservation].
factory ReservationModel.fromEntity(Reservation reservation) {
return ReservationModel(
id: reservation.id,
userId: reservation.userId,
userFullName: reservation.userFullName,
eventId: reservation.eventId,
eventTitle: reservation.eventTitle,
reservationDate: reservation.reservationDate,
numberOfPeople: reservation.numberOfPeople,
status: reservation.status,
establishmentId: reservation.establishmentId,
establishmentName: reservation.establishmentName,
notes: reservation.notes,
createdAt: reservation.createdAt,
);
}
final String id;
final String userId;
final String userFullName;
final String eventId;
final String eventTitle;
final DateTime reservationDate;
final int numberOfPeople;
final ReservationStatus status;
final String? establishmentId;
final String? establishmentName;
final String? notes;
final DateTime? createdAt;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'userFullName': userFullName,
'eventId': eventId,
'eventTitle': eventTitle,
'reservationDate': reservationDate.toIso8601String(),
'numberOfPeople': numberOfPeople,
'status': _statusToString(status),
if (establishmentId != null) 'establishmentId': establishmentId,
if (establishmentName != null) 'establishmentName': establishmentName,
if (notes != null) 'notes': notes,
if (createdAt != null) 'createdAt': createdAt!.toIso8601String(),
};
}
/// Convertit ce modèle vers une entité de domaine [Reservation].
Reservation toEntity() {
return Reservation(
id: id,
userId: userId,
userFullName: userFullName,
eventId: eventId,
eventTitle: eventTitle,
reservationDate: reservationDate,
numberOfPeople: numberOfPeople,
status: status,
establishmentId: establishmentId,
establishmentName: establishmentName,
notes: notes,
createdAt: createdAt,
);
}
/// Parse une valeur string depuis le JSON avec valeur par défaut.
static String _parseString(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
return json[key] as String? ?? defaultValue;
}
/// Parse une valeur int depuis le JSON avec valeur par défaut 1.
static int _parseInt(Map<String, dynamic> json, String key) {
return json[key] as int? ?? 1;
}
/// Parse un timestamp depuis le JSON.
static DateTime _parseTimestamp(dynamic timestamp) {
if (timestamp == null) return DateTime.now();
if (timestamp is String) {
try {
return DateTime.parse(timestamp);
} catch (e) {
return DateTime.now();
}
}
if (timestamp is int) {
return DateTime.fromMillisecondsSinceEpoch(timestamp);
}
return DateTime.now();
}
/// Parse un ID (UUID) depuis le JSON.
static String _parseId(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
/// Parse le statut de réservation depuis le JSON.
static ReservationStatus _parseStatus(String? status) {
if (status == null) return ReservationStatus.pending;
switch (status.toLowerCase()) {
case 'pending':
case 'en attente':
return ReservationStatus.pending;
case 'confirmed':
case 'confirmé':
case 'confirmée':
return ReservationStatus.confirmed;
case 'cancelled':
case 'annulé':
case 'annulée':
return ReservationStatus.cancelled;
case 'completed':
case 'terminé':
case 'terminée':
return ReservationStatus.completed;
default:
return ReservationStatus.pending;
}
}
/// Convertit le statut en string pour l'API.
static String _statusToString(ReservationStatus status) {
switch (status) {
case ReservationStatus.pending:
return 'pending';
case ReservationStatus.confirmed:
return 'confirmed';
case ReservationStatus.cancelled:
return 'cancelled';
case ReservationStatus.completed:
return 'completed';
}
}
}

View File

@@ -1,23 +1,153 @@
class SocialPost {
final String userName;
final String userImage;
final String postText;
final String postImage;
final int likes;
final int comments;
final int shares;
final List<String> badges; // Gamification badges
final List<String> tags; // Ajout de tags pour personnalisation des posts
import '../../domain/entities/social_post.dart';
SocialPost({
required this.userName,
required this.userImage,
required this.postText,
required this.postImage,
required this.likes,
required this.comments,
required this.shares,
required this.badges,
this.tags = const [],
/// Modèle de données pour les posts sociaux (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 SocialPost.
class SocialPostModel {
SocialPostModel({
required this.id,
required this.content,
required this.userId,
required this.userFirstName,
required this.userLastName,
required this.userProfileImageUrl,
required this.timestamp,
this.imageUrl,
this.likesCount = 0,
this.commentsCount = 0,
this.sharesCount = 0,
this.isLikedByCurrentUser = false,
});
/// Factory pour créer un [SocialPostModel] à partir d'un JSON.
factory SocialPostModel.fromJson(Map<String, dynamic> json) {
return SocialPostModel(
id: _parseId(json, 'id', ''),
content: _parseString(json, 'content', ''),
userId: _parseId(json, 'userId', ''),
userFirstName: _parseString(json, 'userFirstName', ''),
userLastName: _parseString(json, 'userLastName', ''),
userProfileImageUrl: _parseString(json, 'userProfileImageUrl', ''),
timestamp: _parseTimestamp(json['timestamp']),
imageUrl: json['imageUrl'] as String?,
likesCount: _parseInt(json, 'likesCount'),
commentsCount: _parseInt(json, 'commentsCount'),
sharesCount: _parseInt(json, 'sharesCount'),
isLikedByCurrentUser: json['isLikedByCurrentUser'] as bool? ?? false,
);
}
/// Crée un [SocialPostModel] depuis une entité de domaine [SocialPost].
factory SocialPostModel.fromEntity(SocialPost post) {
return SocialPostModel(
id: post.id,
content: post.content,
userId: post.userId,
userFirstName: post.userFirstName,
userLastName: post.userLastName,
userProfileImageUrl: post.userProfileImageUrl,
timestamp: post.timestamp,
imageUrl: post.imageUrl,
likesCount: post.likesCount,
commentsCount: post.commentsCount,
sharesCount: post.sharesCount,
isLikedByCurrentUser: post.isLikedByCurrentUser,
);
}
final String id;
final String content;
final String userId;
final String userFirstName;
final String userLastName;
final String userProfileImageUrl;
final DateTime timestamp;
final String? imageUrl;
final int likesCount;
final int commentsCount;
final int sharesCount;
final bool isLikedByCurrentUser;
/// Convertit ce modèle en JSON pour l'envoi vers l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
'userId': userId,
'userFirstName': userFirstName,
'userLastName': userLastName,
'userProfileImageUrl': userProfileImageUrl,
'timestamp': timestamp.toIso8601String(),
if (imageUrl != null) 'imageUrl': imageUrl,
'likesCount': likesCount,
'commentsCount': commentsCount,
'sharesCount': sharesCount,
'isLikedByCurrentUser': isLikedByCurrentUser,
};
}
/// Convertit ce modèle vers une entité de domaine [SocialPost].
SocialPost toEntity() {
return SocialPost(
id: id,
content: content,
userId: userId,
userFirstName: userFirstName,
userLastName: userLastName,
userProfileImageUrl: userProfileImageUrl,
timestamp: timestamp,
imageUrl: imageUrl,
likesCount: likesCount,
commentsCount: commentsCount,
sharesCount: sharesCount,
isLikedByCurrentUser: isLikedByCurrentUser,
);
}
/// Parse une valeur string depuis le JSON avec valeur par défaut.
static String _parseString(
Map<String, dynamic> json,
String key,
String defaultValue,
) {
return json[key] as String? ?? defaultValue;
}
/// Parse une valeur int depuis le JSON avec valeur par défaut 0.
static int _parseInt(Map<String, dynamic> json, String key) {
return json[key] as int? ?? 0;
}
/// Parse un timestamp depuis le JSON.
static DateTime _parseTimestamp(dynamic timestamp) {
if (timestamp == null) return DateTime.now();
if (timestamp is String) {
try {
return DateTime.parse(timestamp);
} catch (e) {
return DateTime.now();
}
}
if (timestamp is int) {
return DateTime.fromMillisecondsSinceEpoch(timestamp);
}
return DateTime.now();
}
/// Parse un ID (UUID) depuis le JSON.
///
/// [json] Le JSON à parser
/// [key] La clé de l'ID
/// [defaultValue] La valeur par défaut si l'ID est null
///
/// Returns l'ID parsé ou la valeur par défaut
static String _parseId(Map<String, dynamic> json, String key, String defaultValue) {
final value = json[key];
if (value == null) return defaultValue;
return value.toString();
}
}

View File

@@ -0,0 +1,148 @@
import '../../core/constants/env_config.dart';
import '../../core/utils/app_logger.dart';
import '../../domain/entities/story.dart';
/// Modèle de données pour les stories (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 [Story].
class StoryModel extends Story {
/// Crée une nouvelle instance de [StoryModel].
const StoryModel({
required super.id,
required super.userId,
required super.userFirstName,
required super.userLastName,
required super.userProfileImageUrl,
required super.userIsVerified,
required super.mediaType,
required super.mediaUrl,
required super.createdAt,
required super.expiresAt,
super.thumbnailUrl,
super.durationSeconds,
super.isActive,
super.viewsCount,
super.hasViewed,
});
/// Crée un [StoryModel] à partir d'un JSON reçu depuis l'API.
factory StoryModel.fromJson(Map<String, dynamic> json) {
try {
return StoryModel(
id: json['id']?.toString() ?? '',
userId: json['userId']?.toString() ?? '',
userFirstName: json['userFirstName']?.toString() ?? '',
userLastName: json['userLastName']?.toString() ?? '',
userProfileImageUrl: json['userProfileImageUrl']?.toString() ?? '',
userIsVerified: json['userIsVerified'] as bool? ?? false,
mediaType: _parseMediaType(json['mediaType']),
mediaUrl: json['mediaUrl']?.toString() ?? '',
thumbnailUrl: json['thumbnailUrl']?.toString(),
durationSeconds: json['durationSeconds'] as int?,
createdAt: _parseDateTime(json['createdAt']),
expiresAt: _parseDateTime(json['expiresAt']),
isActive: json['isActive'] as bool? ?? true,
viewsCount: json['viewsCount'] as int? ?? 0,
hasViewed: json['hasViewed'] as bool? ?? false,
);
} catch (e, stackTrace) {
AppLogger.e('Erreur lors du parsing JSON', error: e, stackTrace: stackTrace, tag: 'StoryModel');
AppLogger.d('JSON reçu: $json', tag: 'StoryModel');
rethrow;
}
}
/// Convertit le modèle en JSON pour l'envoi à l'API.
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'userFirstName': userFirstName,
'userLastName': userLastName,
'userProfileImageUrl': userProfileImageUrl,
'userIsVerified': userIsVerified,
'mediaType': _mediaTypeToString(mediaType),
'mediaUrl': mediaUrl,
'thumbnailUrl': thumbnailUrl,
'durationSeconds': durationSeconds,
'createdAt': createdAt.toIso8601String(),
'expiresAt': expiresAt.toIso8601String(),
'isActive': isActive,
'viewsCount': viewsCount,
'hasViewed': hasViewed,
};
}
/// Convertit le modèle en entité de domaine.
Story toEntity() {
return Story(
id: id,
userId: userId,
userFirstName: userFirstName,
userLastName: userLastName,
userProfileImageUrl: userProfileImageUrl,
userIsVerified: userIsVerified,
mediaType: mediaType,
mediaUrl: mediaUrl,
thumbnailUrl: thumbnailUrl,
durationSeconds: durationSeconds,
createdAt: createdAt,
expiresAt: expiresAt,
isActive: isActive,
viewsCount: viewsCount,
hasViewed: hasViewed,
);
}
/// Parse le type de média depuis une string.
static StoryMediaType _parseMediaType(dynamic value) {
if (value == null) return StoryMediaType.image;
final stringValue = value.toString().toUpperCase();
switch (stringValue) {
case 'IMAGE':
return StoryMediaType.image;
case 'VIDEO':
return StoryMediaType.video;
default:
AppLogger.w('Type de média inconnu: $value, utilisation de IMAGE par défaut', tag: 'StoryModel');
return StoryMediaType.image;
}
}
/// Convertit le type de média en string pour l'API.
static String _mediaTypeToString(StoryMediaType type) {
switch (type) {
case StoryMediaType.image:
return 'IMAGE';
case StoryMediaType.video:
return 'VIDEO';
}
}
/// Parse une DateTime depuis différents formats possibles.
static DateTime _parseDateTime(dynamic value) {
if (value == null) return DateTime.now();
try {
// Si c'est déjà une DateTime
if (value is DateTime) return value;
// Si c'est une string ISO 8601
if (value is String) {
return DateTime.parse(value);
}
// Si c'est un timestamp en millisecondes
if (value is int) {
return DateTime.fromMillisecondsSinceEpoch(value);
}
AppLogger.w('Format de date non reconnu: $value', tag: 'StoryModel');
return DateTime.now();
} catch (e, stackTrace) {
AppLogger.e('Erreur parsing DateTime', error: e, stackTrace: stackTrace, tag: 'StoryModel');
return DateTime.now();
}
}
}

View File

@@ -1,4 +1,5 @@
import '../../core/constants/env_config.dart';
import '../../core/utils/app_logger.dart';
import '../../domain/entities/user.dart';
/// Modèle de données pour les utilisateurs (Data Transfer Object).
@@ -37,6 +38,7 @@ class UserModel extends User {
required super.email,
required super.motDePasse,
required super.profileImageUrl,
super.isVerified,
super.eventsCount,
super.friendsCount,
super.postsCount,
@@ -75,15 +77,14 @@ class UserModel extends User {
email: _parseString(json, 'email', ''),
motDePasse: _parseString(json, 'motDePasse', ''),
profileImageUrl: _parseString(json, 'profileImageUrl', ''),
isVerified: json['isVerified'] as bool? ?? false,
eventsCount: _parseInt(json, 'eventsCount') ?? 0,
friendsCount: _parseInt(json, 'friendsCount') ?? 0,
postsCount: _parseInt(json, 'postsCount') ?? 0,
visitedPlacesCount: _parseInt(json, 'visitedPlacesCount') ?? 0,
);
} catch (e) {
if (EnvConfig.enableDetailedLogs) {
print('[UserModel] Erreur lors du parsing JSON: $e');
}
} catch (e, stackTrace) {
AppLogger.e('Erreur lors du parsing JSON', error: e, stackTrace: stackTrace, tag: 'UserModel');
rethrow;
}
}