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:
396
lib/data/datasources/chat_remote_data_source.dart
Normal file
396
lib/data/datasources/chat_remote_data_source.dart
Normal file
@@ -0,0 +1,396 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/chat_message_model.dart';
|
||||
import '../models/conversation_model.dart';
|
||||
|
||||
/// Source de données distante pour le chat.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations liées au chat
|
||||
/// via l'API backend (REST) et WebSocket pour le temps réel.
|
||||
class ChatRemoteDataSource {
|
||||
ChatRemoteDataSource(this.client);
|
||||
|
||||
final http.Client client;
|
||||
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
AppLogger.http(method, uri.toString());
|
||||
|
||||
try {
|
||||
http.Response response;
|
||||
final requestHeaders = {..._defaultHeaders, ...?headers};
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client.get(uri, headers: requestHeaders).timeout(_timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client.post(uri, headers: requestHeaders, body: body).timeout(_timeout);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client.put(uri, headers: requestHeaders, body: body).timeout(_timeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client.delete(uri, headers: requestHeaders).timeout(_timeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'ChatRemoteDataSource');
|
||||
|
||||
return response;
|
||||
} on TimeoutException catch (e, stackTrace) {
|
||||
AppLogger.e('Timeout lors de la requête $method $uri', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
throw ServerException('La requête a pris trop de temps. Le serveur ne répond pas.');
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
throw const ServerException('Erreur de connexion réseau. Vérifiez votre connexion internet.');
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _parseJsonResponse(http.Response response, List<int> expectedStatusCodes) {
|
||||
if (expectedStatusCodes.contains(response.statusCode)) {
|
||||
if (response.body.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
final errorMessage = (json.decode(response.body) as Map<String, dynamic>?)?['message'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
|
||||
AppLogger.e('Erreur (${response.statusCode}): $errorMessage', tag: 'ChatRemoteDataSource');
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw ServerException('Ressource non trouvée: $errorMessage');
|
||||
default:
|
||||
throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère toutes les conversations d'un utilisateur.
|
||||
Future<List<ConversationModel>> getConversations(String userId) async {
|
||||
AppLogger.d('Récupération des conversations pour: $userId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getUserConversations(userId));
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => ConversationModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des conversations', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère ou crée une conversation avec un utilisateur.
|
||||
///
|
||||
/// Si la conversation n'existe pas (404) ou si le backend ne répond pas (timeout),
|
||||
/// elle sera créée automatiquement en envoyant un message initial.
|
||||
Future<ConversationModel> getOrCreateConversation(String userId, String participantId) async {
|
||||
AppLogger.d('Récupération/création conversation: $userId <-> $participantId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
final uri = Uri.parse(Urls.getConversationBetweenUsers(userId, participantId));
|
||||
|
||||
try {
|
||||
// Essayer de récupérer la conversation existante avec un timeout plus court
|
||||
final response = await _performRequestWithTimeout('GET', uri, timeout: const Duration(seconds: 10));
|
||||
|
||||
// Si la conversation existe, la retourner
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
|
||||
return ConversationModel.fromJson(jsonResponse);
|
||||
}
|
||||
|
||||
// Si 404, la conversation n'existe pas, on doit la créer
|
||||
if (response.statusCode == 404) {
|
||||
AppLogger.i('Conversation non trouvée (404), création en cours...', tag: 'ChatRemoteDataSource');
|
||||
return await _createConversationBySendingMessage(userId, participantId, uri);
|
||||
}
|
||||
|
||||
// Pour les autres codes d'erreur, utiliser la méthode standard de parsing
|
||||
final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map<String, dynamic>;
|
||||
return ConversationModel.fromJson(jsonResponse);
|
||||
} on ServerException catch (e) {
|
||||
// Si c'est une ServerException avec 404 ou timeout, on essaie de créer la conversation
|
||||
if (e.message.contains('404') ||
|
||||
e.message.contains('non trouvée') ||
|
||||
e.message.contains('not found') ||
|
||||
e.message.contains('trop de temps') ||
|
||||
e.message.contains('ne répond pas')) {
|
||||
AppLogger.i('Conversation non trouvée ou timeout, création en cours...', tag: 'ChatRemoteDataSource');
|
||||
return await _createConversationBySendingMessage(userId, participantId, uri);
|
||||
}
|
||||
rethrow;
|
||||
} on TimeoutException catch (e, stackTrace) {
|
||||
// Si timeout, créer directement la conversation
|
||||
AppLogger.w('Timeout lors de la récupération de la conversation, création directe...', tag: 'ChatRemoteDataSource');
|
||||
return await _createConversationBySendingMessage(userId, participantId, uri);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération/création de conversation', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Effectue une requête HTTP avec un timeout personnalisé.
|
||||
Future<http.Response> _performRequestWithTimeout(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
AppLogger.http(method, uri.toString());
|
||||
|
||||
try {
|
||||
http.Response response;
|
||||
final requestHeaders = {..._defaultHeaders, ...?headers};
|
||||
final requestTimeout = timeout ?? _timeout;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client.get(uri, headers: requestHeaders).timeout(requestTimeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client.post(uri, headers: requestHeaders, body: body).timeout(requestTimeout);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client.put(uri, headers: requestHeaders, body: body).timeout(requestTimeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client.delete(uri, headers: requestHeaders).timeout(requestTimeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'ChatRemoteDataSource');
|
||||
|
||||
return response;
|
||||
} on TimeoutException catch (e, stackTrace) {
|
||||
AppLogger.e('Timeout lors de la requête $method $uri', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
throw ServerException('La requête a pris trop de temps. Le serveur ne répond pas.');
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
throw const ServerException('Erreur de connexion réseau. Vérifiez votre connexion internet.');
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une conversation en envoyant un message initial, puis récupère la conversation créée.
|
||||
Future<ConversationModel> _createConversationBySendingMessage(
|
||||
String userId,
|
||||
String participantId,
|
||||
Uri conversationUri,
|
||||
) async {
|
||||
try {
|
||||
// Créer la conversation en envoyant un message initial
|
||||
// Le backend créera automatiquement la conversation lors de l'envoi du premier message
|
||||
AppLogger.i('Création de la conversation en envoyant un message initial...', tag: 'ChatRemoteDataSource');
|
||||
|
||||
await sendMessage(
|
||||
senderId: userId,
|
||||
recipientId: participantId,
|
||||
content: '👋', // Message initial pour créer la conversation
|
||||
messageType: 'text',
|
||||
);
|
||||
|
||||
AppLogger.i('Message initial envoyé, attente de la création de la conversation...', tag: 'ChatRemoteDataSource');
|
||||
|
||||
// Attendre un peu pour que le backend crée la conversation
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
||||
// Récupérer la conversation nouvellement créée avec plusieurs tentatives et timeout court
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
AppLogger.d('Tentative ${attempt + 1}/3 pour récupérer la conversation créée...', tag: 'ChatRemoteDataSource');
|
||||
final retryResponse = await _performRequestWithTimeout(
|
||||
'GET',
|
||||
conversationUri,
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
if (retryResponse.statusCode == 200 || retryResponse.statusCode == 201) {
|
||||
final jsonResponse = json.decode(retryResponse.body) as Map<String, dynamic>;
|
||||
AppLogger.i('Conversation créée avec succès', tag: 'ChatRemoteDataSource');
|
||||
return ConversationModel.fromJson(jsonResponse);
|
||||
}
|
||||
} on TimeoutException {
|
||||
AppLogger.w('Timeout lors de la tentative ${attempt + 1}, nouvelle tentative...', tag: 'ChatRemoteDataSource');
|
||||
}
|
||||
|
||||
// Attendre un peu plus avant la prochaine tentative
|
||||
if (attempt < 2) {
|
||||
await Future.delayed(Duration(milliseconds: 500 * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
// Si après 3 tentatives on n'a toujours pas la conversation, lever une exception
|
||||
throw ServerException(
|
||||
'Impossible de créer ou récupérer la conversation après plusieurs tentatives. '
|
||||
'Le serveur ne répond pas correctement.',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e(
|
||||
'Erreur lors de la création de la conversation via message initial',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
tag: 'ChatRemoteDataSource',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère tous les messages d'une conversation.
|
||||
Future<List<ChatMessageModel>> getMessages(String conversationId, {int page = 0, int size = 50}) async {
|
||||
AppLogger.d('Récupération des messages: $conversationId (page: $page)', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getConversationMessages(conversationId, page: page, size: size));
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => ChatMessageModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des messages', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoie un nouveau message dans une conversation.
|
||||
Future<ChatMessageModel> sendMessage({
|
||||
required String senderId,
|
||||
required String recipientId,
|
||||
required String content,
|
||||
String? messageType,
|
||||
String? mediaUrl,
|
||||
}) async {
|
||||
AppLogger.i('Envoi message de $senderId à $recipientId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.sendMessage);
|
||||
final body = json.encode({
|
||||
'senderId': senderId,
|
||||
'recipientId': recipientId,
|
||||
'content': content,
|
||||
'messageType': messageType ?? 'text',
|
||||
if (mediaUrl != null) 'mediaUrl': mediaUrl,
|
||||
});
|
||||
final response = await _performRequest('POST', uri, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map<String, dynamic>;
|
||||
return ChatMessageModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de l\'envoi du message', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque un message comme lu.
|
||||
Future<void> markMessageAsRead(String messageId) async {
|
||||
AppLogger.d('Marquer message comme lu: $messageId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.markMessageAsRead(messageId));
|
||||
await _performRequest('PUT', uri);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du marquage comme lu', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque tous les messages d'une conversation comme lus.
|
||||
Future<void> markConversationAsRead(String conversationId, String userId) async {
|
||||
AppLogger.d('Marquer conversation comme lue: $conversationId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.markAllMessagesAsRead(conversationId, userId));
|
||||
await _performRequest('PUT', uri);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du marquage de la conversation', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime un message.
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
AppLogger.i('Suppression message: $messageId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.deleteMessage(messageId));
|
||||
await _performRequest('DELETE', uri);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une conversation.
|
||||
Future<void> deleteConversation(String conversationId) async {
|
||||
AppLogger.i('Suppression conversation: $conversationId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.deleteConversation(conversationId));
|
||||
await _performRequest('DELETE', uri);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression de la conversation', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère le nombre de messages non lus pour un utilisateur.
|
||||
Future<int> getUnreadMessagesCount(String userId) async {
|
||||
AppLogger.d('Récupération du nombre de messages non lus pour: $userId', tag: 'ChatRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getUnreadMessagesCount(userId));
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return jsonResponse['unreadCount'] as int? ?? 0;
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération du nombre de messages non lus', error: e, stackTrace: stackTrace, tag: 'ChatRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoie un indicateur de frappe (typing indicator) via WebSocket.
|
||||
/// Cette méthode est maintenant gérée par le WebSocket, pas par REST.
|
||||
Future<void> sendTypingIndicator(String recipientId, String userId, bool isTyping) async {
|
||||
AppLogger.d('Indicateur de frappe envoyé via WebSocket pour: $recipientId ($isTyping)', tag: 'ChatRemoteDataSource');
|
||||
// Cette fonctionnalité sera gérée par ChatWebSocketService
|
||||
}
|
||||
}
|
||||
160
lib/data/datasources/establishment_remote_data_source.dart
Normal file
160
lib/data/datasources/establishment_remote_data_source.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/establishment_model.dart';
|
||||
|
||||
/// Source de données distante pour les établissements.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations liées aux établissements
|
||||
/// via l'API backend.
|
||||
class EstablishmentRemoteDataSource {
|
||||
EstablishmentRemoteDataSource(this.client);
|
||||
|
||||
final http.Client client;
|
||||
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
AppLogger.http(method, uri.toString());
|
||||
|
||||
try {
|
||||
http.Response response;
|
||||
final requestHeaders = {..._defaultHeaders, ...?headers};
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client.get(uri, headers: requestHeaders).timeout(_timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client.post(uri, headers: requestHeaders, body: body).timeout(_timeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'EstablishmentRemoteDataSource');
|
||||
|
||||
return response;
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
throw const ServerException('Erreur de connexion réseau. Vérifiez votre connexion internet.');
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _parseJsonResponse(http.Response response, List<int> expectedStatusCodes) {
|
||||
if (expectedStatusCodes.contains(response.statusCode)) {
|
||||
if (response.body.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
final errorMessage = (json.decode(response.body) as Map<String, dynamic>?)?['message'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
|
||||
AppLogger.e('Erreur (${response.statusCode}): $errorMessage', tag: 'EstablishmentRemoteDataSource');
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw ServerException('Établissement non trouvé: $errorMessage');
|
||||
default:
|
||||
throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère tous les établissements.
|
||||
Future<List<EstablishmentModel>> getAllEstablishments() async {
|
||||
AppLogger.d('Récupération de tous les établissements', tag: 'EstablishmentRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/establishments');
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => EstablishmentModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des établissements', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche des établissements par nom ou ville.
|
||||
Future<List<EstablishmentModel>> searchEstablishments(String query) async {
|
||||
AppLogger.d('Recherche: $query', tag: 'EstablishmentRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/establishments/search').replace(
|
||||
queryParameters: {'q': query},
|
||||
);
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => EstablishmentModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la recherche', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtre les établissements par type et/ou fourchette de prix.
|
||||
Future<List<EstablishmentModel>> filterEstablishments({
|
||||
String? type,
|
||||
String? priceRange,
|
||||
String? city,
|
||||
}) async {
|
||||
AppLogger.d('Filtrage: type=$type, priceRange=$priceRange, city=$city', tag: 'EstablishmentRemoteDataSource');
|
||||
|
||||
try {
|
||||
final queryParams = <String, String>{};
|
||||
if (type != null) queryParams['type'] = type;
|
||||
if (priceRange != null) queryParams['priceRange'] = priceRange;
|
||||
if (city != null) queryParams['city'] = city;
|
||||
|
||||
final uri = Uri.parse('${Urls.baseUrl}/establishments').replace(queryParameters: queryParams);
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => EstablishmentModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du filtrage', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère un établissement par son ID.
|
||||
Future<EstablishmentModel> getEstablishmentById(String id) async {
|
||||
AppLogger.d('Récupération établissement: $id', tag: 'EstablishmentRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/establishments/$id');
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return EstablishmentModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération de l\'établissement', error: e, stackTrace: stackTrace, tag: 'EstablishmentRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,264 +1,674 @@
|
||||
import 'dart:convert';
|
||||
import 'package:afterwork/core/constants/urls.dart';
|
||||
import 'package:afterwork/data/models/event_model.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/event_model.dart';
|
||||
|
||||
/// Source de données pour les événements distants.
|
||||
/// Source de données distante pour les événements.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations CRUD sur les événements
|
||||
/// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts,
|
||||
/// et la validation des réponses.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final dataSource = EventRemoteDataSource(http.Client());
|
||||
/// final events = await dataSource.getAllEvents();
|
||||
/// ```
|
||||
class EventRemoteDataSource {
|
||||
final http.Client client;
|
||||
|
||||
/// Crée une nouvelle instance de [EventRemoteDataSource].
|
||||
///
|
||||
/// [client] Le client HTTP à utiliser pour les requêtes
|
||||
EventRemoteDataSource(this.client);
|
||||
|
||||
/// Récupérer tous les événements depuis l'API.
|
||||
Future<List<EventModel>> getAllEvents() async {
|
||||
print('Récupération de tous les événements depuis ${Urls.baseUrl}/events');
|
||||
/// Client HTTP pour effectuer les requêtes réseau
|
||||
final http.Client client;
|
||||
|
||||
final response = await client.get(Uri.parse('${Urls.baseUrl}/events'));
|
||||
/// Headers par défaut pour les requêtes
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
/// Timeout pour les requêtes réseau
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonResponse = json.decode(response.body);
|
||||
print('Réponse JSON reçue: $jsonResponse');
|
||||
return jsonResponse.map((event) => EventModel.fromJson(event)).toList();
|
||||
} else {
|
||||
print('Erreur lors de la récupération des événements: ${response.body}');
|
||||
throw ServerException();
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// MÉTHODES PRIVÉES UTILITAIRES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupérer les événements créés par un utilisateur spécifique et ses amis.
|
||||
/// Cette méthode envoie une requête POST au serveur pour obtenir la liste des événements créés
|
||||
/// par l'utilisateur spécifié et ses amis, en utilisant l'identifiant de l'utilisateur.
|
||||
/// Effectue une requête HTTP avec gestion d'erreurs et timeout.
|
||||
///
|
||||
/// [userId] : L'identifiant de l'utilisateur pour lequel récupérer les événements.
|
||||
/// Retourne une liste de modèles d'événements [EventModel].
|
||||
Future<List<EventModel>> getEventsCreatedByUserAndFriends(String userId) async {
|
||||
// Log de début de la méthode pour signaler l'initialisation de la récupération des événements
|
||||
print('[LOG] Démarrage de la récupération des événements créés par l\'utilisateur ID: $userId et ses amis.');
|
||||
/// [method] La méthode HTTP (GET, POST, PUT, DELETE, PATCH)
|
||||
/// [uri] L'URI de la requête
|
||||
/// [headers] Les headers de la requête
|
||||
/// [body] Le corps de la requête (optionnel)
|
||||
///
|
||||
/// Returns la réponse HTTP
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
try {
|
||||
final requestHeaders = {
|
||||
..._defaultHeaders,
|
||||
if (headers != null) ...headers,
|
||||
};
|
||||
|
||||
// Construction de l'URL de l'API pour la requête POST
|
||||
final url = Uri.parse('${Urls.baseUrl}/events/created-by-user-and-friends');
|
||||
print('[LOG] URL construite pour la requête: $url');
|
||||
http.Response response;
|
||||
|
||||
// Création de l'en-tête de la requête, spécifiant que le contenu est en JSON
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
print('[LOG] En-têtes de la requête: $headers');
|
||||
|
||||
// Construction du corps de la requête en JSON, incluant l'identifiant de l'utilisateur
|
||||
final body = jsonEncode({'userId': userId});
|
||||
print('[LOG] Corps de la requête JSON: $body');
|
||||
|
||||
// Envoi de la requête POST au serveur pour récupérer les événements
|
||||
final response = await client.post(url, headers: headers, body: body);
|
||||
print('[LOG] Requête POST envoyée au serveur.');
|
||||
|
||||
// Vérification et log de l'état de la réponse reçue
|
||||
print('[LOG] Statut de la réponse HTTP: ${response.statusCode}');
|
||||
|
||||
// Gestion de la réponse en fonction du code de statut
|
||||
if (response.statusCode == 200) {
|
||||
// Déchiffrement du JSON reçu si le code de statut est 200 (OK)
|
||||
final List<dynamic> jsonResponse = json.decode(response.body);
|
||||
print('[LOG] Réponse JSON complète reçue (taille: ${jsonResponse.length}) :');
|
||||
|
||||
// Affichage détaillé de chaque événement
|
||||
for (var i = 0; i < jsonResponse.length; i++) {
|
||||
final event = jsonResponse[i];
|
||||
print('[LOG] Événement $i :');
|
||||
print(' - ID: ${event['id']}');
|
||||
print(' - Titre: ${event['title']}');
|
||||
print(' - Description: ${event['description']}');
|
||||
print(' - Date de début: ${event['startDate']}');
|
||||
print(' - Date de fin: ${event['endDate']}');
|
||||
print(' - Localisation: ${event['location']}');
|
||||
print(' - Catégorie: ${event['category']}');
|
||||
print(' - Lien: ${event['link']}');
|
||||
print(' - URL de l\'image: ${event['imageUrl']}');
|
||||
print(' - Statut: ${event['status']}');
|
||||
print(' - prenom du créateur: ${event['creatorFirstName']}');
|
||||
print(' - prenom du créateur: ${event['creatorLastName']}');
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
response = await client
|
||||
.get(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client
|
||||
.post(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client
|
||||
.put(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client
|
||||
.delete(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'PATCH':
|
||||
response = await client
|
||||
.patch(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
// Transformation du JSON en une liste d'objets EventModel
|
||||
List<EventModel> events = jsonResponse.map((event) => EventModel.fromJson(event)).toList();
|
||||
print('[LOG] Conversion JSON -> List<EventModel> réussie. Nombre d\'événements: ${events.length}');
|
||||
return response;
|
||||
} on SocketException {
|
||||
throw ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion Internet.',
|
||||
statusCode: null,
|
||||
);
|
||||
} on HttpException catch (e) {
|
||||
throw ServerException(
|
||||
'Erreur HTTP: ${e.message}',
|
||||
statusCode: null,
|
||||
);
|
||||
} on FormatException catch (e) {
|
||||
throw ServerException(
|
||||
'Erreur de format de réponse: ${e.message}',
|
||||
statusCode: null,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is ServerException) rethrow;
|
||||
throw ServerException(
|
||||
'Erreur inattendue: $e',
|
||||
statusCode: null,
|
||||
originalError: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Retourne la liste d'événements si tout s'est bien passé
|
||||
/// Parse une réponse JSON et gère les erreurs.
|
||||
///
|
||||
/// [response] La réponse HTTP
|
||||
/// [expectedStatusCodes] Les codes de statut attendus (par défaut: [200])
|
||||
///
|
||||
/// Returns les données JSON décodées
|
||||
///
|
||||
/// Throws [ServerException] si le code de statut n'est pas attendu
|
||||
dynamic _parseJsonResponse(
|
||||
http.Response response,
|
||||
List<int> expectedStatusCodes,
|
||||
) {
|
||||
if (!expectedStatusCodes.contains(response.statusCode)) {
|
||||
_handleErrorResponse(response);
|
||||
}
|
||||
|
||||
try {
|
||||
if (response.body.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} on FormatException catch (e) {
|
||||
throw ServerException(
|
||||
'Erreur de parsing JSON: ${e.message}',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les erreurs de réponse HTTP.
|
||||
///
|
||||
/// [response] La réponse HTTP avec erreur
|
||||
///
|
||||
/// Throws [ServerException] avec un message approprié
|
||||
void _handleErrorResponse(http.Response response) {
|
||||
String errorMessage;
|
||||
|
||||
try {
|
||||
final errorBody = json.decode(response.body);
|
||||
errorMessage = errorBody['message'] as String? ??
|
||||
errorBody['error'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
} catch (e) {
|
||||
errorMessage = response.body.isNotEmpty
|
||||
? response.body
|
||||
: 'Erreur serveur (${response.statusCode})';
|
||||
}
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 400:
|
||||
throw ValidationException(errorMessage);
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw ServerException(
|
||||
'Ressource non trouvée',
|
||||
statusCode: 404,
|
||||
);
|
||||
case 409:
|
||||
throw ConflictException(errorMessage);
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
throw ServerException(
|
||||
'Erreur serveur: $errorMessage',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
default:
|
||||
throw ServerException(
|
||||
errorMessage,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log un message si le mode debug est activé.
|
||||
///
|
||||
/// [message] Le message à logger
|
||||
void _log(String message) {
|
||||
AppLogger.d(message, tag: 'EventRemoteDataSource');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PUBLIQUES - CRUD ÉVÉNEMENTS
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère tous les événements depuis l'API.
|
||||
///
|
||||
/// Returns une liste de [EventModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final events = await dataSource.getAllEvents();
|
||||
/// ```
|
||||
Future<List<EventModel>> getAllEvents() async {
|
||||
_log('Récupération de tous les événements');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getAllEvents);
|
||||
final response = await _performRequest('GET', uri);
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>?;
|
||||
|
||||
if (jsonResponse == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final events = jsonResponse
|
||||
.map((json) => EventModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
_log('${events.length} événements récupérés avec succès');
|
||||
return events;
|
||||
} else {
|
||||
// Log et gestion de l'erreur en cas de statut HTTP autre que 200
|
||||
print('[ERROR] Erreur lors de la récupération des événements: ${response.body}');
|
||||
throw ServerException('[ERROR] Échec de récupération des événements créés par l\'utilisateur $userId et ses amis.');
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la récupération des événements: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Créer un nouvel événement via l'API.
|
||||
Future<EventModel> createEvent(EventModel event) async {
|
||||
print('Création d\'un nouvel événement avec les données: ${event.toJson()}');
|
||||
/// Récupère les événements créés par un utilisateur et ses amis.
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Returns une liste de [EventModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final events = await dataSource.getEventsCreatedByUserAndFriends('user123');
|
||||
/// ```
|
||||
Future<List<EventModel>> getEventsCreatedByUserAndFriends(
|
||||
String userId,
|
||||
) async {
|
||||
_log('Récupération des événements pour l\'utilisateur $userId et ses amis');
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse(Urls.createEvent),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(event.toJson()),
|
||||
);
|
||||
if (userId.isEmpty) {
|
||||
throw ValidationException('L\'ID utilisateur ne peut pas être vide');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getEventsCreatedByUserAndFriends);
|
||||
final body = jsonEncode({'userId': userId});
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
print('Événement créé avec succès');
|
||||
return EventModel.fromJson(json.decode(response.body));
|
||||
} else {
|
||||
print('Erreur lors de la création de l\'événement: ${response.body}');
|
||||
throw ServerException();
|
||||
final response = await _performRequest(
|
||||
'POST',
|
||||
uri,
|
||||
body: body,
|
||||
);
|
||||
|
||||
// Gérer le cas 404 (aucun ami trouvé) comme une liste vide
|
||||
if (response.statusCode == 404) {
|
||||
_log('Aucun événement trouvé (404) - retour d\'une liste vide');
|
||||
return [];
|
||||
}
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>?;
|
||||
|
||||
if (jsonResponse == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final events = jsonResponse
|
||||
.map((json) => EventModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
_log('${events.length} événements récupérés avec succès');
|
||||
return events;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la récupération des événements: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer un événement spécifique par son ID.
|
||||
/// Récupère les événements de l'utilisateur et de ses amis (avec pagination).
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
/// [page] Le numéro de la page (0-indexé)
|
||||
/// [size] La taille de la page
|
||||
///
|
||||
/// Returns une liste de [EventModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final events = await dataSource.getEventsByFriends(
|
||||
/// userId: 'user123',
|
||||
/// page: 0,
|
||||
/// size: 20,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<EventModel>> getEventsByFriends({
|
||||
required String userId,
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
}) async {
|
||||
_log('Récupération des événements des amis pour $userId (page: $page, size: $size)');
|
||||
|
||||
if (userId.isEmpty) {
|
||||
throw ValidationException('L\'ID utilisateur ne peut pas être vide');
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'${Urls.getEventsByFriends(userId)}?page=$page&size=$size',
|
||||
);
|
||||
|
||||
final response = await _performRequest('GET', uri);
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>?;
|
||||
|
||||
if (jsonResponse == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final events = jsonResponse
|
||||
.map((json) => EventModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
_log('${events.length} événements des amis récupérés avec succès');
|
||||
return events;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la récupération des événements des amis: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère un événement par son ID.
|
||||
///
|
||||
/// [id] L'identifiant de l'événement
|
||||
///
|
||||
/// Returns un [EventModel]
|
||||
///
|
||||
/// Throws [ServerException] si l'événement n'est pas trouvé
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final event = await dataSource.getEventById('event123');
|
||||
/// ```
|
||||
Future<EventModel> getEventById(String id) async {
|
||||
print('Récupération de l\'événement avec l\'ID: $id');
|
||||
_log('Récupération de l\'événement $id');
|
||||
|
||||
final response = await client.get(Uri.parse('${Urls.getEventById}/$id'));
|
||||
if (id.isEmpty) {
|
||||
throw ValidationException('L\'ID de l\'événement ne peut pas être vide');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getEventByIdWithId(id));
|
||||
final response = await _performRequest('GET', uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('Événement récupéré avec succès');
|
||||
return EventModel.fromJson(json.decode(response.body));
|
||||
} else {
|
||||
print('Erreur lors de la récupération de l\'événement: ${response.body}');
|
||||
throw ServerException();
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
|
||||
final event = EventModel.fromJson(jsonResponse);
|
||||
_log('Événement $id récupéré avec succès');
|
||||
return event;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la récupération de l\'événement $id: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mettre à jour un événement existant.
|
||||
/// Crée un nouvel événement.
|
||||
///
|
||||
/// [event] Le modèle d'événement à créer
|
||||
///
|
||||
/// Returns le [EventModel] créé avec l'ID généré par le serveur
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final newEvent = EventModel(...);
|
||||
/// final createdEvent = await dataSource.createEvent(newEvent);
|
||||
/// ```
|
||||
Future<EventModel> createEvent(EventModel event) async {
|
||||
_log('Création d\'un nouvel événement: ${event.title}');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.createEvent);
|
||||
final body = jsonEncode(event.toJson());
|
||||
|
||||
final response = await _performRequest(
|
||||
'POST',
|
||||
uri,
|
||||
body: body,
|
||||
);
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [201, 200]) as Map<String, dynamic>;
|
||||
|
||||
final createdEvent = EventModel.fromJson(jsonResponse);
|
||||
_log('Événement créé avec succès: ${createdEvent.id}');
|
||||
return createdEvent;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la création de l\'événement: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour un événement existant.
|
||||
///
|
||||
/// [id] L'identifiant de l'événement à mettre à jour
|
||||
/// [event] Le modèle d'événement avec les nouvelles données
|
||||
///
|
||||
/// Returns le [EventModel] mis à jour
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final updatedEvent = event.copyWith(title: 'Nouveau titre');
|
||||
/// final result = await dataSource.updateEvent('event123', updatedEvent);
|
||||
/// ```
|
||||
Future<EventModel> updateEvent(String id, EventModel event) async {
|
||||
print('Mise à jour de l\'événement avec l\'ID: $id, données: ${event.toJson()}');
|
||||
_log('Mise à jour de l\'événement $id');
|
||||
|
||||
final response = await client.put(
|
||||
Uri.parse('${Urls.updateEvent}/$id'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(event.toJson()),
|
||||
);
|
||||
if (id.isEmpty) {
|
||||
throw ValidationException('L\'ID de l\'événement ne peut pas être vide');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
final uri = Uri.parse(Urls.updateEventWithId(id));
|
||||
final body = jsonEncode(event.toJson());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('Événement mis à jour avec succès');
|
||||
return EventModel.fromJson(json.decode(response.body));
|
||||
} else {
|
||||
print('Erreur lors de la mise à jour de l\'événement: ${response.body}');
|
||||
throw ServerException();
|
||||
final response = await _performRequest(
|
||||
'PUT',
|
||||
uri,
|
||||
body: body,
|
||||
);
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
|
||||
final updatedEvent = EventModel.fromJson(jsonResponse);
|
||||
_log('Événement $id mis à jour avec succès');
|
||||
return updatedEvent;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la mise à jour de l\'événement $id: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprimer un événement par son ID.
|
||||
/// Supprime un événement.
|
||||
///
|
||||
/// [id] L'identifiant de l'événement à supprimer
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// await dataSource.deleteEvent('event123');
|
||||
/// ```
|
||||
Future<void> deleteEvent(String id) async {
|
||||
print('Suppression de l\'événement avec l\'ID: $id');
|
||||
_log('Suppression de l\'événement $id');
|
||||
|
||||
final response = await client.delete(Uri.parse('${Urls.deleteEvent}/$id'));
|
||||
if (id.isEmpty) {
|
||||
throw ValidationException('L\'ID de l\'événement ne peut pas être vide');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
final uri = Uri.parse(Urls.deleteEventWithId(id));
|
||||
final response = await _performRequest('DELETE', uri);
|
||||
|
||||
if (response.statusCode != 204) {
|
||||
print('Erreur lors de la suppression de l\'événement: ${response.body}');
|
||||
throw ServerException();
|
||||
} else {
|
||||
print('Événement supprimé avec succès');
|
||||
if (![200, 204].contains(response.statusCode)) {
|
||||
_handleErrorResponse(response);
|
||||
}
|
||||
|
||||
_log('Événement $id supprimé avec succès');
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la suppression de l\'événement $id: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Participer à un événement.
|
||||
// ============================================================================
|
||||
// MÉTHODES PUBLIQUES - ACTIONS SUR ÉVÉNEMENTS
|
||||
// ============================================================================
|
||||
|
||||
/// Participe à un événement (utilise l'endpoint participants du backend).
|
||||
///
|
||||
/// [eventId] L'identifiant de l'événement
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Returns le [EventModel] mis à jour avec le nouveau participant
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<EventModel> participateInEvent(String eventId, String userId) async {
|
||||
print('Participation à l\'événement avec l\'ID: $eventId, utilisateur: $userId');
|
||||
_log('Participation de l\'utilisateur $userId à l\'événement $eventId');
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('${Urls.addParticipant}/$eventId/participate'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'userId': userId}),
|
||||
);
|
||||
if (eventId.isEmpty || userId.isEmpty) {
|
||||
throw ValidationException('Les IDs ne peuvent pas être vides');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
// Utiliser l'endpoint participants du backend
|
||||
// Le backend attend un objet Users avec l'id
|
||||
final uri = Uri.parse(Urls.participateInEventWithId(eventId));
|
||||
final body = jsonEncode({
|
||||
'id': userId,
|
||||
// Le backend peut aussi accepter juste l'id selon l'implémentation
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('Participation réussie');
|
||||
return EventModel.fromJson(json.decode(response.body));
|
||||
} else {
|
||||
print('Erreur lors de la participation à l\'événement: ${response.body}');
|
||||
throw ServerException();
|
||||
final response = await _performRequest(
|
||||
'POST',
|
||||
uri,
|
||||
body: body,
|
||||
);
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
|
||||
final updatedEvent = EventModel.fromJson(jsonResponse);
|
||||
_log('Participation réussie');
|
||||
return updatedEvent;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la participation: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Réagir à un événement.
|
||||
/// Réagit à un événement (utilise l'endpoint favorite du backend).
|
||||
///
|
||||
/// [eventId] L'identifiant de l'événement
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> reactToEvent(String eventId, String userId) async {
|
||||
print('Réaction à l\'événement avec l\'ID: $eventId, utilisateur: $userId');
|
||||
_log('Réaction de l\'utilisateur $userId à l\'événement $eventId');
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('${Urls.baseUrl}/$eventId/react'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'userId': userId}),
|
||||
);
|
||||
if (eventId.isEmpty || userId.isEmpty) {
|
||||
throw ValidationException('Les IDs ne peuvent pas être vides');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
// Utiliser l'endpoint favorite du backend comme réaction
|
||||
final uri = Uri.parse(Urls.reactToEventWithId(eventId, userId));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
print('Erreur lors de la réaction à l\'événement: ${response.body}');
|
||||
throw ServerException();
|
||||
} else {
|
||||
print('Réaction réussie');
|
||||
final response = await _performRequest('POST', uri);
|
||||
|
||||
if (![200, 201].contains(response.statusCode)) {
|
||||
_handleErrorResponse(response);
|
||||
}
|
||||
|
||||
_log('Réaction enregistrée avec succès');
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la réaction: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fermer un événement.
|
||||
/// Ferme un événement.
|
||||
///
|
||||
/// [eventId] L'identifiant de l'événement à fermer
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// await dataSource.closeEvent('event123');
|
||||
/// ```
|
||||
Future<void> closeEvent(String eventId) async {
|
||||
print('Fermeture de l\'événement avec l\'ID: $eventId');
|
||||
_log('Fermeture de l\'événement $eventId');
|
||||
|
||||
final response = await client.patch(
|
||||
Uri.parse('${Urls.closeEvent}/$eventId/close'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
if (eventId.isEmpty) {
|
||||
throw ValidationException('L\'ID de l\'événement ne peut pas être vide');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
final uri = Uri.parse(Urls.closeEventWithId(eventId));
|
||||
final response = await _performRequest('PATCH', uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('Événement fermé avec succès');
|
||||
} else if (response.statusCode == 400) {
|
||||
final responseBody = json.decode(response.body);
|
||||
final errorMessage = responseBody['message'] ?? 'Erreur inconnue';
|
||||
print('Erreur lors de la fermeture de l\'événement: $errorMessage');
|
||||
throw ServerExceptionWithMessage(errorMessage);
|
||||
} else {
|
||||
print('Erreur lors de la fermeture de l\'événement: ${response.body}');
|
||||
throw ServerExceptionWithMessage('Une erreur est survenue lors de la fermeture de l\'événement.');
|
||||
if (response.statusCode != 200) {
|
||||
_handleErrorResponse(response);
|
||||
}
|
||||
|
||||
_log('Événement $eventId fermé avec succès');
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la fermeture de l\'événement $eventId: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rouvrir un événement.
|
||||
/// Rouvre un événement.
|
||||
///
|
||||
/// [eventId] L'identifiant de l'événement à rouvrir
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// await dataSource.reopenEvent('event123');
|
||||
/// ```
|
||||
Future<void> reopenEvent(String eventId) async {
|
||||
print('Réouverture de l\'événement avec l\'ID: $eventId');
|
||||
_log('Réouverture de l\'événement $eventId');
|
||||
|
||||
final response = await client.patch(
|
||||
Uri.parse('${Urls.reopenEvent}/$eventId/reopen'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
if (eventId.isEmpty) {
|
||||
throw ValidationException('L\'ID de l\'événement ne peut pas être vide');
|
||||
}
|
||||
|
||||
print('Statut de la réponse: ${response.statusCode}');
|
||||
try {
|
||||
final uri = Uri.parse(Urls.reopenEventWithId(eventId));
|
||||
final response = await _performRequest('PATCH', uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('Événement rouvert avec succès');
|
||||
} else if (response.statusCode == 400) {
|
||||
final responseBody = json.decode(response.body);
|
||||
final errorMessage = responseBody['message'] ?? 'Erreur inconnue';
|
||||
print('Erreur lors de la réouverture de l\'événement: $errorMessage');
|
||||
throw ServerExceptionWithMessage(errorMessage);
|
||||
} else if (response.statusCode == 404) {
|
||||
print('L\'événement n\'a pas été trouvé.');
|
||||
throw ServerExceptionWithMessage('L\'événement n\'existe pas ou a déjà été supprimé.');
|
||||
} else {
|
||||
print('Erreur lors de la réouverture de l\'événement: ${response.body}');
|
||||
throw ServerExceptionWithMessage('Une erreur est survenue lors de la réouverture de l\'événement.');
|
||||
if (response.statusCode != 200) {
|
||||
_handleErrorResponse(response);
|
||||
}
|
||||
|
||||
_log('Événement $eventId rouvert avec succès');
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la réouverture de l\'événement $eventId: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche des événements par mot-clé.
|
||||
///
|
||||
/// [keyword] Le mot-clé à rechercher dans le titre et la description
|
||||
///
|
||||
/// Returns une liste de [EventModel] correspondant à la recherche
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<EventModel>> searchEvents(String keyword) async {
|
||||
_log('Recherche d\'événements avec le mot-clé: $keyword');
|
||||
|
||||
if (keyword.trim().isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.searchEvents}?keyword=${Uri.encodeComponent(keyword)}');
|
||||
final response = await _performRequest('GET', uri);
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>?;
|
||||
|
||||
if (jsonResponse == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final events = jsonResponse
|
||||
.map((json) => EventModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
_log('${events.length} événements trouvés pour "$keyword"');
|
||||
return events;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la recherche: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
204
lib/data/datasources/notification_remote_data_source.dart
Normal file
204
lib/data/datasources/notification_remote_data_source.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/notification_model.dart';
|
||||
|
||||
/// Source de données distante pour les notifications.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations liées aux notifications
|
||||
/// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts,
|
||||
/// et la validation des réponses.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final dataSource = NotificationRemoteDataSource(http.Client());
|
||||
/// final notifications = await dataSource.getNotifications(userId);
|
||||
/// ```
|
||||
class NotificationRemoteDataSource {
|
||||
/// Crée une nouvelle instance de [NotificationRemoteDataSource].
|
||||
///
|
||||
/// [client] Le client HTTP à utiliser pour les requêtes
|
||||
NotificationRemoteDataSource(this.client);
|
||||
|
||||
/// Client HTTP pour effectuer les requêtes réseau
|
||||
final http.Client client;
|
||||
|
||||
/// Headers par défaut pour les requêtes
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
/// Timeout pour les requêtes réseau
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PRIVÉES UTILITAIRES
|
||||
// ============================================================================
|
||||
|
||||
/// Effectue une requête HTTP avec gestion d'erreurs et timeout.
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
AppLogger.http(method, uri.toString());
|
||||
|
||||
try {
|
||||
http.Response response;
|
||||
final requestHeaders = {..._defaultHeaders, ...?headers};
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client
|
||||
.get(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client
|
||||
.post(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client
|
||||
.put(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client
|
||||
.delete(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'NotificationRemoteDataSource');
|
||||
|
||||
return response;
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse la réponse JSON et gère les codes de statut HTTP.
|
||||
dynamic _parseJsonResponse(http.Response response, List<int> expectedStatusCodes) {
|
||||
if (expectedStatusCodes.contains(response.statusCode)) {
|
||||
if (response.body.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
final errorMessage = (json.decode(response.body) as Map<String, dynamic>?)?['message'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
|
||||
AppLogger.e('Erreur (${response.statusCode}): $errorMessage', tag: 'NotificationRemoteDataSource');
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw ServerException('Notifications non trouvées: $errorMessage');
|
||||
default:
|
||||
throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PUBLIQUES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère toutes les notifications d'un utilisateur.
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Returns une liste de [NotificationModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<NotificationModel>> getNotifications(String userId) async {
|
||||
AppLogger.d('Récupération des notifications pour $userId', tag: 'NotificationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/notifications/user/$userId');
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => NotificationModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des notifications', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque une notification comme lue.
|
||||
///
|
||||
/// [notificationId] L'identifiant de la notification
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> markAsRead(String notificationId) async {
|
||||
AppLogger.d('Marquage comme lue: $notificationId', tag: 'NotificationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/notifications/$notificationId/read');
|
||||
final response = await _performRequest('PUT', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du marquage comme lue', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque toutes les notifications comme lues.
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> markAllAsRead(String userId) async {
|
||||
AppLogger.d('Marquage toutes comme lues pour $userId', tag: 'NotificationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/notifications/user/$userId/mark-all-read');
|
||||
final response = await _performRequest('PUT', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du marquage toutes comme lues', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une notification.
|
||||
///
|
||||
/// [notificationId] L'identifiant de la notification
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> deleteNotification(String notificationId) async {
|
||||
AppLogger.i('Suppression: $notificationId', tag: 'NotificationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/notifications/$notificationId');
|
||||
final response = await _performRequest('DELETE', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression', error: e, stackTrace: stackTrace, tag: 'NotificationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
232
lib/data/datasources/reservation_remote_data_source.dart
Normal file
232
lib/data/datasources/reservation_remote_data_source.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/reservation_model.dart';
|
||||
|
||||
/// Source de données distante pour les réservations.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations liées aux réservations
|
||||
/// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts,
|
||||
/// et la validation des réponses.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final dataSource = ReservationRemoteDataSource(http.Client());
|
||||
/// final reservations = await dataSource.getReservationsByUser(userId);
|
||||
/// ```
|
||||
class ReservationRemoteDataSource {
|
||||
/// Crée une nouvelle instance de [ReservationRemoteDataSource].
|
||||
///
|
||||
/// [client] Le client HTTP à utiliser pour les requêtes
|
||||
ReservationRemoteDataSource(this.client);
|
||||
|
||||
/// Client HTTP pour effectuer les requêtes réseau
|
||||
final http.Client client;
|
||||
|
||||
/// Headers par défaut pour les requêtes
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
/// Timeout pour les requêtes réseau
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PRIVÉES UTILITAIRES
|
||||
// ============================================================================
|
||||
|
||||
/// Effectue une requête HTTP avec gestion d'erreurs et timeout.
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
AppLogger.http(method, uri.toString());
|
||||
|
||||
try {
|
||||
http.Response response;
|
||||
final requestHeaders = {..._defaultHeaders, ...?headers};
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client
|
||||
.get(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client
|
||||
.post(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client
|
||||
.put(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client
|
||||
.delete(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
return response;
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur inattendue', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse la réponse JSON et gère les codes de statut HTTP.
|
||||
dynamic _parseJsonResponse(http.Response response, List<int> expectedStatusCodes) {
|
||||
if (expectedStatusCodes.contains(response.statusCode)) {
|
||||
if (response.body.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
final errorMessage = (json.decode(response.body) as Map<String, dynamic>?)?['message'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
|
||||
AppLogger.e('Erreur (${response.statusCode}): $errorMessage', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw ServerException('Réservation non trouvée: $errorMessage');
|
||||
default:
|
||||
throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PUBLIQUES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère toutes les réservations d'un utilisateur.
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Returns une liste de [ReservationModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<ReservationModel>> getReservationsByUser(String userId) async {
|
||||
AppLogger.d('Récupération des réservations pour $userId', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/reservations/user/$userId');
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => ReservationModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des réservations', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une nouvelle réservation.
|
||||
///
|
||||
/// [reservation] Le modèle de réservation à créer
|
||||
///
|
||||
/// Returns le [ReservationModel] créé
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<ReservationModel> createReservation(ReservationModel reservation) async {
|
||||
AppLogger.i('Création réservation: ${reservation.eventTitle}', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/reservations');
|
||||
final body = jsonEncode(reservation.toJson());
|
||||
final response = await _performRequest('POST', uri, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map<String, dynamic>;
|
||||
return ReservationModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la création de la réservation', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour une réservation existante.
|
||||
///
|
||||
/// [reservation] Le modèle de réservation avec les modifications
|
||||
///
|
||||
/// Returns le [ReservationModel] mis à jour
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<ReservationModel> updateReservation(ReservationModel reservation) async {
|
||||
AppLogger.i('Mise à jour réservation: ${reservation.id}', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/reservations/${reservation.id}');
|
||||
final body = jsonEncode(reservation.toJson());
|
||||
final response = await _performRequest('PUT', uri, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return ReservationModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la mise à jour de la réservation', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Annule une réservation.
|
||||
///
|
||||
/// [reservationId] L'identifiant de la réservation à annuler
|
||||
///
|
||||
/// Returns le [ReservationModel] avec le statut annulé
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<ReservationModel> cancelReservation(String reservationId) async {
|
||||
AppLogger.i('Annulation réservation: $reservationId', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/reservations/$reservationId/cancel');
|
||||
final response = await _performRequest('PUT', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return ReservationModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de l\'annulation de la réservation', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une réservation.
|
||||
///
|
||||
/// [reservationId] L'identifiant de la réservation à supprimer
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> deleteReservation(String reservationId) async {
|
||||
AppLogger.i('Suppression réservation: $reservationId', tag: 'ReservationRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.baseUrl}/reservations/$reservationId');
|
||||
final response = await _performRequest('DELETE', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression de la réservation', error: e, stackTrace: stackTrace, tag: 'ReservationRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
393
lib/data/datasources/social_remote_data_source.dart
Normal file
393
lib/data/datasources/social_remote_data_source.dart
Normal file
@@ -0,0 +1,393 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/comment_model.dart';
|
||||
import '../models/social_post_model.dart';
|
||||
|
||||
/// Source de données distante pour les posts sociaux.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations liées aux posts sociaux
|
||||
/// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts,
|
||||
/// et la validation des réponses.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final dataSource = SocialRemoteDataSource(http.Client());
|
||||
/// final posts = await dataSource.getPosts();
|
||||
/// ```
|
||||
class SocialRemoteDataSource {
|
||||
/// Crée une nouvelle instance de [SocialRemoteDataSource].
|
||||
///
|
||||
/// [client] Le client HTTP à utiliser pour les requêtes
|
||||
SocialRemoteDataSource(this.client);
|
||||
|
||||
/// Client HTTP pour effectuer les requêtes réseau
|
||||
final http.Client client;
|
||||
|
||||
/// Headers par défaut pour les requêtes
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
/// Timeout pour les requêtes réseau
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PRIVÉES UTILITAIRES
|
||||
// ============================================================================
|
||||
|
||||
/// Effectue une requête HTTP avec gestion d'erreurs et timeout.
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
AppLogger.http(method, uri.toString());
|
||||
|
||||
try {
|
||||
http.Response response;
|
||||
final requestHeaders = {..._defaultHeaders, ...?headers};
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client
|
||||
.get(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client
|
||||
.post(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client
|
||||
.put(uri, headers: requestHeaders, body: body)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client
|
||||
.delete(uri, headers: requestHeaders)
|
||||
.timeout(_timeout);
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'SocialRemoteDataSource');
|
||||
|
||||
return response;
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la requête', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse la réponse JSON et gère les codes de statut HTTP.
|
||||
dynamic _parseJsonResponse(http.Response response, List<int> expectedStatusCodes) {
|
||||
if (expectedStatusCodes.contains(response.statusCode)) {
|
||||
if (response.body.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
final errorMessage = (json.decode(response.body) as Map<String, dynamic>?)?['message'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
|
||||
AppLogger.e('Erreur (${response.statusCode}): $errorMessage', tag: 'SocialRemoteDataSource');
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw ServerException('Post non trouvé: $errorMessage');
|
||||
case 409:
|
||||
throw ConflictException(errorMessage);
|
||||
default:
|
||||
throw ServerException('Erreur serveur (${response.statusCode}): $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PUBLIQUES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère tous les posts sociaux.
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur (optionnel, pour filtrer)
|
||||
///
|
||||
/// Returns une liste de [SocialPostModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<SocialPostModel>> getPosts({String? userId}) async {
|
||||
AppLogger.d('Récupération des posts', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = userId != null
|
||||
? Uri.parse(Urls.getSocialPostsByUserId(userId))
|
||||
: Uri.parse(Urls.getAllPosts);
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => SocialPostModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des posts', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les posts de l'utilisateur et de ses amis.
|
||||
///
|
||||
/// [userId] L'identifiant de l'utilisateur
|
||||
/// [page] Le numéro de la page (0-indexé)
|
||||
/// [size] La taille de la page
|
||||
///
|
||||
/// Returns une liste de [SocialPostModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<SocialPostModel>> getPostsByFriends({
|
||||
required String userId,
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
}) async {
|
||||
AppLogger.d('Récupération des posts des amis pour $userId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'${Urls.getSocialPostsByFriends(userId)}?page=$page&size=$size',
|
||||
);
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => SocialPostModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des posts des amis', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouveau post social.
|
||||
///
|
||||
/// [content] Le contenu du post
|
||||
/// [userId] L'identifiant de l'utilisateur créateur
|
||||
/// [imageUrl] URL de l'image (optionnel)
|
||||
///
|
||||
/// Returns le [SocialPostModel] créé
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<SocialPostModel> createPost({
|
||||
required String content,
|
||||
required String userId,
|
||||
String? imageUrl,
|
||||
}) async {
|
||||
AppLogger.i('Création de post pour $userId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.createSocialPost);
|
||||
final body = jsonEncode({
|
||||
'content': content,
|
||||
'userId': userId,
|
||||
if (imageUrl != null) 'imageUrl': imageUrl,
|
||||
});
|
||||
final response = await _performRequest('POST', uri, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [201]) as Map<String, dynamic>;
|
||||
return SocialPostModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la création du post', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche des posts sociaux.
|
||||
///
|
||||
/// [query] Le terme de recherche
|
||||
///
|
||||
/// Returns une liste de [SocialPostModel] correspondant à la recherche
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<SocialPostModel>> searchPosts(String query) async {
|
||||
AppLogger.d('Recherche: $query', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.searchSocialPostsWithQuery(query));
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => SocialPostModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la recherche', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Like un post.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
///
|
||||
/// Returns le [SocialPostModel] mis à jour
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<SocialPostModel> likePost(String postId) async {
|
||||
AppLogger.d('Like du post: $postId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.likeSocialPostWithId(postId));
|
||||
final response = await _performRequest('POST', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return SocialPostModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du like', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un commentaire à un post.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
///
|
||||
/// Returns le [SocialPostModel] mis à jour
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<SocialPostModel> commentPost(String postId) async {
|
||||
AppLogger.d('Commentaire sur le post: $postId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.commentSocialPostWithId(postId));
|
||||
final response = await _performRequest('POST', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return SocialPostModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Partage un post.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
///
|
||||
/// Returns le [SocialPostModel] mis à jour
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<SocialPostModel> sharePost(String postId) async {
|
||||
AppLogger.d('Partage du post: $postId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.shareSocialPostWithId(postId));
|
||||
final response = await _performRequest('POST', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return SocialPostModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime un post.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> deletePost(String postId) async {
|
||||
AppLogger.i('Suppression du post: $postId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.deleteSocialPostWithId(postId));
|
||||
final response = await _performRequest('DELETE', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression du post', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES POUR LES COMMENTAIRES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère tous les commentaires d'un post.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
///
|
||||
/// Returns une liste de [CommentModel]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<List<CommentModel>> getComments(String postId) async {
|
||||
AppLogger.d('Récupération des commentaires pour le post: $postId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getCommentsForPost(postId));
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonList = _parseJsonResponse(response, [200]) as List;
|
||||
return jsonList.map((json) => CommentModel.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des commentaires', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouveau commentaire sur un post.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
/// [content] Le contenu du commentaire
|
||||
/// [userId] L'ID de l'utilisateur créateur
|
||||
///
|
||||
/// Returns le [CommentModel] créé
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<CommentModel> createComment({
|
||||
required String postId,
|
||||
required String content,
|
||||
required String userId,
|
||||
}) async {
|
||||
AppLogger.i('Création de commentaire pour le post: $postId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.commentSocialPostWithId(postId));
|
||||
final body = jsonEncode({
|
||||
'content': content,
|
||||
'userId': userId,
|
||||
});
|
||||
final response = await _performRequest('POST', uri, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [200, 201]) as Map<String, dynamic>;
|
||||
return CommentModel.fromJson(jsonResponse);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la création du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime un commentaire.
|
||||
///
|
||||
/// [postId] L'ID du post
|
||||
/// [commentId] L'ID du commentaire
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
Future<void> deleteComment(String postId, String commentId) async {
|
||||
AppLogger.i('Suppression du commentaire: $commentId', tag: 'SocialRemoteDataSource');
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${Urls.getCommentsForPost(postId)}/$commentId');
|
||||
final response = await _performRequest('DELETE', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression du commentaire', error: e, stackTrace: stackTrace, tag: 'SocialRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
244
lib/data/datasources/story_remote_data_source.dart
Normal file
244
lib/data/datasources/story_remote_data_source.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/story_model.dart';
|
||||
|
||||
/// Source de données distante pour les stories.
|
||||
///
|
||||
/// Cette classe gère toutes les opérations liées aux stories
|
||||
/// via l'API backend. Elle inclut la gestion d'erreurs, les timeouts,
|
||||
/// et la validation des réponses.
|
||||
class StoryRemoteDataSource {
|
||||
/// Crée une nouvelle instance de [StoryRemoteDataSource].
|
||||
StoryRemoteDataSource(this.client);
|
||||
|
||||
/// Client HTTP pour effectuer les requêtes réseau
|
||||
final http.Client client;
|
||||
|
||||
/// Headers par défaut pour les requêtes
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
/// Timeout pour les requêtes réseau
|
||||
Duration get _timeout => Duration(seconds: EnvConfig.networkTimeout);
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PUBLIQUES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère toutes les stories actives.
|
||||
///
|
||||
/// [viewerId] ID de l'utilisateur actuel (optionnel) pour marquer les stories vues
|
||||
Future<List<StoryModel>> getStories({String? viewerId}) async {
|
||||
AppLogger.d('Récupération de toutes les stories actives', tag: 'StoryRemoteDataSource');
|
||||
|
||||
try {
|
||||
var uri = Uri.parse(Urls.getAllStories);
|
||||
if (viewerId != null) {
|
||||
uri = Uri.parse('${Urls.getAllStories}?viewerId=$viewerId');
|
||||
}
|
||||
|
||||
final response = await client
|
||||
.get(uri, headers: _defaultHeaders)
|
||||
.timeout(_timeout);
|
||||
|
||||
return _handleListResponse(response);
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des stories', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les stories d'un utilisateur spécifique.
|
||||
///
|
||||
/// [userId] L'ID de l'utilisateur
|
||||
/// [viewerId] ID de l'utilisateur actuel (optionnel)
|
||||
Future<List<StoryModel>> getStoriesByUserId(
|
||||
String userId, {
|
||||
String? viewerId,
|
||||
}) async {
|
||||
AppLogger.d('Récupération des stories pour l\'utilisateur: $userId', tag: 'StoryRemoteDataSource');
|
||||
|
||||
try {
|
||||
var uri = Uri.parse(Urls.getStoriesByUserId(userId));
|
||||
if (viewerId != null) {
|
||||
uri = Uri.parse('${Urls.getStoriesByUserId(userId)}?viewerId=$viewerId');
|
||||
}
|
||||
|
||||
final response = await client
|
||||
.get(uri, headers: _defaultHeaders)
|
||||
.timeout(_timeout);
|
||||
|
||||
return _handleListResponse(response);
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des stories par utilisateur', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une nouvelle story.
|
||||
///
|
||||
/// [userId] L'ID de l'utilisateur créateur
|
||||
/// [mediaType] Le type de média (IMAGE ou VIDEO)
|
||||
/// [mediaUrl] L'URL du média
|
||||
/// [thumbnailUrl] L'URL du thumbnail (optionnel, pour vidéos)
|
||||
/// [durationSeconds] Durée en secondes (optionnel, pour vidéos)
|
||||
Future<StoryModel> createStory({
|
||||
required String userId,
|
||||
required String mediaType,
|
||||
required String mediaUrl,
|
||||
String? thumbnailUrl,
|
||||
int? durationSeconds,
|
||||
}) async {
|
||||
AppLogger.i('Création d\'une story', tag: 'StoryRemoteDataSource');
|
||||
|
||||
try {
|
||||
final body = jsonEncode({
|
||||
'userId': userId,
|
||||
'mediaType': mediaType,
|
||||
'mediaUrl': mediaUrl,
|
||||
if (thumbnailUrl != null) 'thumbnailUrl': thumbnailUrl,
|
||||
if (durationSeconds != null) 'durationSeconds': durationSeconds,
|
||||
});
|
||||
|
||||
final response = await client
|
||||
.post(
|
||||
Uri.parse(Urls.createStory),
|
||||
headers: _defaultHeaders,
|
||||
body: body,
|
||||
)
|
||||
.timeout(_timeout);
|
||||
|
||||
return _handleSingleResponse(response);
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la création de la story', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque une story comme vue.
|
||||
///
|
||||
/// [storyId] L'ID de la story
|
||||
/// [viewerId] L'ID de l'utilisateur qui voit la story
|
||||
Future<StoryModel> markStoryAsViewed(String storyId, String viewerId) async {
|
||||
AppLogger.d('Marquage de la story $storyId comme vue', tag: 'StoryRemoteDataSource');
|
||||
|
||||
try {
|
||||
final response = await client
|
||||
.post(
|
||||
Uri.parse(Urls.markStoryAsViewedWithId(storyId, viewerId)),
|
||||
headers: _defaultHeaders,
|
||||
)
|
||||
.timeout(_timeout);
|
||||
|
||||
return _handleSingleResponse(response);
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du marquage comme vue', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une story.
|
||||
///
|
||||
/// [storyId] L'ID de la story
|
||||
Future<void> deleteStory(String storyId) async {
|
||||
AppLogger.i('Suppression de la story $storyId', tag: 'StoryRemoteDataSource');
|
||||
|
||||
try {
|
||||
final response = await client
|
||||
.delete(
|
||||
Uri.parse(Urls.deleteStoryWithId(storyId)),
|
||||
headers: _defaultHeaders,
|
||||
)
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException(
|
||||
'Erreur lors de la suppression de la story. Code: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
AppLogger.i('Story supprimée avec succès', tag: 'StoryRemoteDataSource');
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
throw const ServerException(
|
||||
'Erreur de connexion réseau. Vérifiez votre connexion internet.',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression', error: e, stackTrace: stackTrace, tag: 'StoryRemoteDataSource');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MÉTHODES PRIVÉES UTILITAIRES
|
||||
// ============================================================================
|
||||
|
||||
/// Gère la réponse pour une liste de stories.
|
||||
List<StoryModel> _handleListResponse(http.Response response) {
|
||||
AppLogger.http('GET', 'stories', statusCode: response.statusCode);
|
||||
AppLogger.d('Response body: ${response.body}', tag: 'StoryRemoteDataSource');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;
|
||||
return jsonList
|
||||
.map((json) => StoryModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else if (response.statusCode == 404) {
|
||||
throw const ServerException('Stories non trouvées.');
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Erreur lors de la récupération des stories. Code: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère la réponse pour une seule story.
|
||||
StoryModel _handleSingleResponse(http.Response response) {
|
||||
AppLogger.http('POST', 'story', statusCode: response.statusCode);
|
||||
AppLogger.d('Response body: ${response.body}', tag: 'StoryRemoteDataSource');
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final Map<String, dynamic> json =
|
||||
jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return StoryModel.fromJson(json);
|
||||
} else if (response.statusCode == 404) {
|
||||
throw const ServerException('Story non trouvée.');
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Erreur lors de l'opération sur la story. Code: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +1,271 @@
|
||||
import 'dart:convert';
|
||||
import 'package:afterwork/core/constants/urls.dart';
|
||||
import 'package:afterwork/data/models/user_model.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// Classe pour gérer les opérations API pour les utilisateurs.
|
||||
/// Toutes les actions sont loguées pour faciliter la traçabilité et le débogage.
|
||||
/// Source de données distante pour les opérations liées aux utilisateurs.
|
||||
///
|
||||
/// Cette classe gère les appels API pour l'authentification, la récupération,
|
||||
/// la création, la mise à jour et la suppression des utilisateurs.
|
||||
/// Elle inclut une gestion robuste des erreurs et des logs détaillés.
|
||||
class UserRemoteDataSource {
|
||||
// Client HTTP injecté pour réaliser les appels réseau
|
||||
final http.Client client;
|
||||
|
||||
/// Constructeur avec injection du client HTTP
|
||||
/// Constructeur avec injection du client HTTP.
|
||||
UserRemoteDataSource(this.client);
|
||||
|
||||
/// Authentifie un utilisateur avec l'email et le mot de passe.
|
||||
/// Si l'authentification réussit, retourne un objet `UserModel`.
|
||||
/// Les erreurs sont gérées et toutes les actions sont loguées.
|
||||
Future<UserModel> authenticateUser(String email, String password) async {
|
||||
print("[LOG] Tentative d'authentification pour l'email : $email");
|
||||
/// Client HTTP utilisé pour les requêtes réseau.
|
||||
final http.Client client;
|
||||
|
||||
/// Exécute une requête HTTP générique avec gestion des erreurs et des logs.
|
||||
///
|
||||
/// [method] : La méthode HTTP (GET, POST, PUT, DELETE, PATCH).
|
||||
/// [uri] : L'URI complète de la requête.
|
||||
/// [headers] : Les en-têtes de la requête (optionnel).
|
||||
/// [body] : Le corps de la requête (optionnel).
|
||||
///
|
||||
/// Retourne la réponse HTTP.
|
||||
/// Lève une [ServerException] ou [SocketException] en cas d'erreur.
|
||||
Future<http.Response> _performRequest(
|
||||
String method,
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
AppLogger.http(method, uri.toString());
|
||||
AppLogger.d('En-têtes: $headers', tag: 'UserRemoteDataSource');
|
||||
AppLogger.d('Corps: $body', tag: 'UserRemoteDataSource');
|
||||
}
|
||||
|
||||
try {
|
||||
// Préparation des données d'authentification à envoyer
|
||||
final Map<String, dynamic> body = {
|
||||
'email': email,
|
||||
'motDePasse': password,
|
||||
};
|
||||
|
||||
print("[DEBUG] Données envoyées pour authentification : $body");
|
||||
|
||||
// Envoi de la requête HTTP POST pour authentifier l'utilisateur
|
||||
final response = await client.post(
|
||||
Uri.parse('${Urls.baseUrl}/users/authenticate'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
// Log de la réponse reçue du serveur
|
||||
print("[LOG] Réponse du serveur : ${response.statusCode} - ${response.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final userData = jsonDecode(response.body);
|
||||
|
||||
if (userData['userId'] != null && userData['userId'].isNotEmpty) {
|
||||
print("[LOG] Utilisateur authentifié avec succès. ID: ${userData['userId']}");
|
||||
return UserModel.fromJson(userData);
|
||||
} else {
|
||||
print("[ERROR] L'ID utilisateur est manquant dans la réponse.");
|
||||
throw Exception("ID utilisateur manquant.");
|
||||
}
|
||||
} else if (response.statusCode == 401) {
|
||||
print("[ERROR] Authentification échouée : Mot de passe incorrect.");
|
||||
throw UnauthorizedException("Mot de passe incorrect.");
|
||||
} else {
|
||||
print("[ERROR] Erreur du serveur. Code : ${response.statusCode}");
|
||||
throw ServerExceptionWithMessage("Erreur inattendue : ${response.body}");
|
||||
http.Response response;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await client.get(uri, headers: headers).timeout(
|
||||
Duration(seconds: EnvConfig.networkTimeout),
|
||||
);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await client
|
||||
.post(uri, headers: headers, body: body)
|
||||
.timeout(Duration(seconds: EnvConfig.networkTimeout));
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await client
|
||||
.put(uri, headers: headers, body: body)
|
||||
.timeout(Duration(seconds: EnvConfig.networkTimeout));
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await client.delete(uri, headers: headers).timeout(
|
||||
Duration(seconds: EnvConfig.networkTimeout),
|
||||
);
|
||||
break;
|
||||
case 'PATCH':
|
||||
response = await client
|
||||
.patch(uri, headers: headers, body: body)
|
||||
.timeout(Duration(seconds: EnvConfig.networkTimeout));
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError('Méthode HTTP non supportée: $method');
|
||||
}
|
||||
} catch (e) {
|
||||
print("[ERROR] Erreur lors de l'authentification : $e");
|
||||
throw Exception("Erreur lors de l'authentification : $e");
|
||||
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
AppLogger.http(method, uri.toString(), statusCode: response.statusCode);
|
||||
AppLogger.d('Réponse: ${response.body}', tag: 'UserRemoteDataSource');
|
||||
}
|
||||
return response;
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion réseau', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource');
|
||||
throw const ServerException('Erreur de connexion réseau. Vérifiez votre connexion internet.');
|
||||
} on http.ClientException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur client HTTP', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource');
|
||||
throw ServerException('Erreur client HTTP: ${e.message}');
|
||||
} on FormatException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de format de réponse JSON', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource');
|
||||
throw const ServerException('Réponse du serveur mal formatée.');
|
||||
} on HandshakeException catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de handshake SSL/TLS', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource');
|
||||
throw const ServerException('Erreur de sécurité: Problème de certificat SSL/TLS.');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur inattendue lors de la requête', error: e, stackTrace: stackTrace, tag: 'UserRemoteDataSource');
|
||||
rethrow; // Rethrow other unexpected exceptions
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse la réponse JSON et gère les codes de statut HTTP.
|
||||
///
|
||||
/// [response] : La réponse HTTP à parser.
|
||||
/// [expectedStatusCodes] : Liste des codes de statut HTTP attendus pour une réponse réussie.
|
||||
///
|
||||
/// Retourne le corps de la réponse décodé.
|
||||
/// Lève des exceptions spécifiques en fonction du code de statut.
|
||||
dynamic _parseJsonResponse(
|
||||
http.Response response,
|
||||
List<int> expectedStatusCodes,
|
||||
) {
|
||||
if (expectedStatusCodes.contains(response.statusCode)) {
|
||||
if (response.body.isEmpty) {
|
||||
return {}; // Retourne un objet vide pour les réponses 204 No Content
|
||||
}
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
final String errorMessage =
|
||||
json.decode(response.body)['message'] as String? ??
|
||||
'Erreur serveur inconnue';
|
||||
AppLogger.e('Erreur API (${response.statusCode}): $errorMessage', tag: 'UserRemoteDataSource');
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
throw UnauthorizedException(errorMessage);
|
||||
case 404:
|
||||
throw UserNotFoundException(errorMessage);
|
||||
case 409:
|
||||
throw ConflictException(errorMessage);
|
||||
default:
|
||||
throw ServerException(
|
||||
'Erreur serveur (${response.statusCode}): $errorMessage',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authentifie un utilisateur avec l'email et le mot de passe.
|
||||
///
|
||||
/// [email] : L'email de l'utilisateur.
|
||||
/// [password] : Le mot de passe de l'utilisateur.
|
||||
///
|
||||
/// Retourne un [UserModel] si l'authentification réussit.
|
||||
/// Lève une [UnauthorizedException] si les identifiants sont incorrects.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<UserModel> authenticateUser(String email, String password) async {
|
||||
final uri = Uri.parse(Urls.authenticateUser);
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final body = jsonEncode({'email': email, 'motDePasse': password});
|
||||
|
||||
final response = await _performRequest('POST', uri, headers: headers, body: body);
|
||||
final userData = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
|
||||
if (userData['userId'] != null && userData['userId'].isNotEmpty) {
|
||||
return UserModel.fromJson(userData);
|
||||
} else {
|
||||
throw const ServerException('ID utilisateur manquant dans la réponse.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère un utilisateur par son identifiant.
|
||||
/// Les erreurs et les succès sont logués pour un suivi complet.
|
||||
///
|
||||
/// [id] : L'identifiant unique de l'utilisateur.
|
||||
///
|
||||
/// Retourne un [UserModel] si l'utilisateur est trouvé.
|
||||
/// Lève une [UserNotFoundException] si l'utilisateur n'existe pas.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<UserModel> getUser(String id) async {
|
||||
print("[LOG] Tentative de récupération de l'utilisateur avec l'ID : $id");
|
||||
final uri = Uri.parse(Urls.getUserByIdWithId(id));
|
||||
final response = await _performRequest('GET', uri);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return UserModel.fromJson(jsonResponse);
|
||||
}
|
||||
|
||||
try {
|
||||
// Envoi de la requête GET pour obtenir l'utilisateur par son ID
|
||||
final response = await client.get(Uri.parse('${Urls.baseUrl}/users/$id'));
|
||||
print("[LOG] Réponse du serveur pour getUser : ${response.statusCode} - ${response.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Utilisateur trouvé, retour de l'objet UserModel
|
||||
return UserModel.fromJson(json.decode(response.body));
|
||||
}
|
||||
// Gestion du cas où l'utilisateur n'est pas trouvé
|
||||
else if (response.statusCode == 404) {
|
||||
print("[ERROR] Utilisateur non trouvé.");
|
||||
throw UserNotFoundException();
|
||||
}
|
||||
// Gestion des autres erreurs serveur
|
||||
else {
|
||||
print("[ERROR] Erreur du serveur lors de la récupération de l'utilisateur.");
|
||||
throw ServerException();
|
||||
}
|
||||
} catch (e) {
|
||||
print("[ERROR] Erreur lors de la récupération de l'utilisateur : $e");
|
||||
throw Exception("Erreur lors de la récupération de l'utilisateur : $e");
|
||||
/// Recherche un utilisateur par email.
|
||||
///
|
||||
/// [email] : L'email de l'utilisateur à rechercher.
|
||||
///
|
||||
/// Retourne un [UserModel] si l'utilisateur est trouvé.
|
||||
/// Lève une [UserNotFoundException] si l'utilisateur n'existe pas.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<UserModel> searchUserByEmail(String email) async {
|
||||
final uri = Uri.parse(Urls.searchUserByEmail(email));
|
||||
final response = await _performRequest('GET', uri);
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
throw UserNotFoundException('Utilisateur non trouvé avec l\'email : $email');
|
||||
}
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return UserModel.fromJson(jsonResponse);
|
||||
}
|
||||
|
||||
/// Crée un nouvel utilisateur dans le backend.
|
||||
/// Toutes les actions, succès ou erreurs sont logués pour un suivi précis.
|
||||
///
|
||||
/// [user] : Le [UserModel] à créer.
|
||||
///
|
||||
/// Retourne le [UserModel] créé avec les données du serveur.
|
||||
/// Lève une [ConflictException] si un utilisateur avec le même email existe déjà.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<UserModel> createUser(UserModel user) async {
|
||||
print("[LOG] Création d'un nouvel utilisateur : ${user.toJson()}");
|
||||
final uri = Uri.parse(Urls.createUser);
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final body = jsonEncode(user.toJson());
|
||||
|
||||
try {
|
||||
// Envoi de la requête POST pour créer un nouvel utilisateur
|
||||
final response = await client.post(
|
||||
Uri.parse('${Urls.baseUrl}/users'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(user.toJson()), // Conversion du modèle utilisateur en JSON
|
||||
);
|
||||
print("[LOG] Réponse du serveur pour createUser : ${response.statusCode} - ${response.body}");
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
// Utilisateur créé avec succès
|
||||
return UserModel.fromJson(json.decode(response.body));
|
||||
}
|
||||
// Gestion des conflits (ex: utilisateur déjà existant)
|
||||
else if (response.statusCode == 409) {
|
||||
print("[ERROR] Conflit lors de la création de l'utilisateur : Utilisateur déjà existant.");
|
||||
throw ConflictException();
|
||||
}
|
||||
// Gestion des autres erreurs serveur
|
||||
else {
|
||||
print("[ERROR] Erreur du serveur lors de la création de l'utilisateur.");
|
||||
throw ServerException();
|
||||
}
|
||||
} catch (e) {
|
||||
print("[ERROR] Erreur lors de la création de l'utilisateur : $e");
|
||||
throw Exception("Erreur lors de la création de l'utilisateur : $e");
|
||||
}
|
||||
final response = await _performRequest('POST', uri, headers: headers, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [201]) as Map<String, dynamic>;
|
||||
return UserModel.fromJson(jsonResponse);
|
||||
}
|
||||
|
||||
/// Met à jour un utilisateur existant.
|
||||
/// Chaque étape est loguée pour faciliter le débogage.
|
||||
///
|
||||
/// [user] : Le [UserModel] avec les données mises à jour.
|
||||
///
|
||||
/// Retourne le [UserModel] mis à jour avec les données du serveur.
|
||||
/// Lève une [UserNotFoundException] si l'utilisateur n'existe pas.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<UserModel> updateUser(UserModel user) async {
|
||||
print("[LOG] Mise à jour de l'utilisateur : ${user.toJson()}");
|
||||
final uri = Uri.parse(Urls.updateUserWithId(user.userId));
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final body = jsonEncode(user.toJson());
|
||||
|
||||
try {
|
||||
// Envoi de la requête PUT pour mettre à jour un utilisateur
|
||||
final response = await client.put(
|
||||
Uri.parse('${Urls.baseUrl}/users/${user.userId}'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(user.toJson()), // Conversion du modèle utilisateur en JSON
|
||||
);
|
||||
print("[LOG] Réponse du serveur pour updateUser : ${response.statusCode} - ${response.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Mise à jour réussie
|
||||
return UserModel.fromJson(json.decode(response.body));
|
||||
}
|
||||
// Gestion du cas où l'utilisateur n'est pas trouvé
|
||||
else if (response.statusCode == 404) {
|
||||
print("[ERROR] Utilisateur non trouvé.");
|
||||
throw UserNotFoundException();
|
||||
}
|
||||
// Gestion des autres erreurs serveur
|
||||
else {
|
||||
print("[ERROR] Erreur du serveur lors de la mise à jour de l'utilisateur.");
|
||||
throw ServerException();
|
||||
}
|
||||
} catch (e) {
|
||||
print("[ERROR] Erreur lors de la mise à jour de l'utilisateur : $e");
|
||||
throw Exception("Erreur lors de la mise à jour de l'utilisateur : $e");
|
||||
}
|
||||
final response = await _performRequest('PUT', uri, headers: headers, body: body);
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>;
|
||||
return UserModel.fromJson(jsonResponse);
|
||||
}
|
||||
|
||||
/// Supprime un utilisateur par son identifiant.
|
||||
/// Les erreurs et succès sont logués pour garantir un suivi complet.
|
||||
///
|
||||
/// [id] : L'identifiant unique de l'utilisateur à supprimer.
|
||||
///
|
||||
/// Ne retourne rien en cas de succès.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<void> deleteUser(String id) async {
|
||||
print("[LOG] Tentative de suppression de l'utilisateur avec l'ID : $id");
|
||||
final uri = Uri.parse(Urls.deleteUserWithId(id));
|
||||
final response = await _performRequest('DELETE', uri);
|
||||
_parseJsonResponse(response, [204]); // 204 No Content
|
||||
}
|
||||
|
||||
try {
|
||||
// Envoi de la requête DELETE pour supprimer un utilisateur
|
||||
final response = await client.delete(Uri.parse('${Urls.baseUrl}/users/$id'));
|
||||
print("[LOG] Réponse du serveur pour deleteUser : ${response.statusCode} - ${response.body}");
|
||||
/// Demande la réinitialisation du mot de passe.
|
||||
///
|
||||
/// [email] : L'email de l'utilisateur qui souhaite réinitialiser son mot de passe.
|
||||
///
|
||||
/// Ne retourne rien en cas de succès.
|
||||
/// Lève une [UserNotFoundException] si l'utilisateur n'existe pas.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
///
|
||||
/// **Note:** Le backend actuel ne supporte pas encore cette fonctionnalité.
|
||||
/// Cette méthode est préparée pour une future implémentation.
|
||||
Future<void> requestPasswordReset(String email) async {
|
||||
// TODO: Implémenter quand l'endpoint sera disponible dans le backend
|
||||
// Le backend actuel a seulement /users/{id}/reset-password qui nécessite l'ID
|
||||
throw const ServerException(
|
||||
'La réinitialisation du mot de passe par email n\'est pas encore disponible. '
|
||||
'Contactez l\'administrateur.',
|
||||
);
|
||||
}
|
||||
|
||||
// Vérification du succès de la suppression
|
||||
if (response.statusCode == 204) {
|
||||
print("[LOG] Utilisateur supprimé avec succès.");
|
||||
}
|
||||
// Gestion des autres erreurs serveur
|
||||
else {
|
||||
print("[ERROR] Erreur du serveur lors de la suppression de l'utilisateur.");
|
||||
throw ServerException();
|
||||
}
|
||||
} catch (e) {
|
||||
print("[ERROR] Erreur lors de la suppression de l'utilisateur : $e");
|
||||
throw Exception("Erreur lors de la suppression de l'utilisateur : $e");
|
||||
}
|
||||
/// Réinitialise le mot de passe d'un utilisateur par son ID.
|
||||
///
|
||||
/// [userId] : L'ID de l'utilisateur.
|
||||
/// [newPassword] : Le nouveau mot de passe.
|
||||
///
|
||||
/// Ne retourne rien en cas de succès.
|
||||
/// Lève une [UserNotFoundException] si l'utilisateur n'existe pas.
|
||||
/// Lève une [ServerException] pour d'autres erreurs serveur.
|
||||
Future<void> resetPasswordById(String userId, String newPassword) async {
|
||||
final uri = Uri.parse('${Urls.getUserByIdWithId(userId)}/reset-password?newPassword=${Uri.encodeComponent(newPassword)}');
|
||||
final response = await _performRequest('PATCH', uri);
|
||||
_parseJsonResponse(response, [200, 204]);
|
||||
}
|
||||
}
|
||||
|
||||
136
lib/data/models/chat_message_model.dart
Normal file
136
lib/data/models/chat_message_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
128
lib/data/models/comment_model.dart
Normal file
128
lib/data/models/comment_model.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
99
lib/data/models/conversation_model.dart
Normal file
99
lib/data/models/conversation_model.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
190
lib/data/models/establishment_model.dart
Normal file
190
lib/data/models/establishment_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
78
lib/data/models/friend_suggestion_model.dart
Normal file
78
lib/data/models/friend_suggestion_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
181
lib/data/models/notification_model.dart
Normal file
181
lib/data/models/notification_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
192
lib/data/models/reservation_model.dart
Normal file
192
lib/data/models/reservation_model.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
148
lib/data/models/story_model.dart
Normal file
148
lib/data/models/story_model.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../data/repositories/friends_repository_impl.dart';
|
||||
import '../../data/services/realtime_notification_service.dart';
|
||||
import '../../data/services/secure_storage.dart';
|
||||
import '../../domain/entities/friend.dart';
|
||||
import '../../domain/entities/friend_request.dart';
|
||||
@@ -15,7 +18,6 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl].
|
||||
FriendsProvider({required this.friendsRepository});
|
||||
final FriendsRepositoryImpl friendsRepository;
|
||||
final Logger _logger = Logger(); // Utilisation du logger pour une traçabilité complète des actions.
|
||||
|
||||
// Liste des amis
|
||||
List<Friend> _friendsList = [];
|
||||
@@ -36,6 +38,14 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
|
||||
final int _requestsPerPage = 10;
|
||||
|
||||
// Liste des suggestions d'amis
|
||||
List<dynamic> _friendSuggestions = [];
|
||||
bool _isLoadingSuggestions = false;
|
||||
|
||||
// Service de notifications temps réel
|
||||
RealtimeNotificationService? _realtimeService;
|
||||
StreamSubscription<FriendRequestNotification>? _friendRequestSubscription;
|
||||
|
||||
// Getters pour accéder à l'état actuel des données
|
||||
bool get isLoading => _isLoading;
|
||||
bool get hasMore => _hasMore;
|
||||
@@ -44,7 +54,9 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
List<FriendRequest> get receivedRequests => _receivedRequests;
|
||||
bool get isLoadingSentRequests => _isLoadingSentRequests;
|
||||
bool get isLoadingReceivedRequests => _isLoadingReceivedRequests;
|
||||
|
||||
List<dynamic> get friendSuggestions => _friendSuggestions;
|
||||
bool get isLoadingSuggestions => _isLoadingSuggestions;
|
||||
|
||||
// Pour compatibilité avec l'ancien code
|
||||
List<FriendRequest> get pendingRequests => _receivedRequests;
|
||||
bool get isLoadingRequests => _isLoadingReceivedRequests;
|
||||
@@ -60,48 +72,48 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// - Les erreurs et les logs pour une traçabilité complète.
|
||||
Future<void> fetchFriends(String userId, {bool loadMore = false}) async {
|
||||
if (_isLoading) {
|
||||
_logger.w('[LOG] Une opération de chargement est déjà en cours. Annulation de la nouvelle requête.');
|
||||
AppLogger.w('Une opération de chargement est déjà en cours. Annulation de la nouvelle requête.', tag: 'FriendsProvider');
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_logger.i('[LOG] Début du chargement des amis pour l\'utilisateur $userId.');
|
||||
AppLogger.i('Début du chargement des amis pour l\'utilisateur $userId.', tag: 'FriendsProvider');
|
||||
|
||||
// Réinitialisation de la pagination si ce n'est pas un chargement supplémentaire
|
||||
if (!loadMore) {
|
||||
_friendsList = [];
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
_logger.i('[LOG] Réinitialisation de la pagination et de la liste des amis.');
|
||||
AppLogger.i('Réinitialisation de la pagination et de la liste des amis.', tag: 'FriendsProvider');
|
||||
}
|
||||
|
||||
try {
|
||||
_logger.i('[LOG] Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.');
|
||||
AppLogger.d('Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.', tag: 'FriendsProvider');
|
||||
final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage);
|
||||
|
||||
// Gestion de l'absence de nouveaux amis
|
||||
if (newFriends.isEmpty) {
|
||||
_hasMore = false;
|
||||
_logger.i('[LOG] Plus d\'amis à charger.');
|
||||
AppLogger.i('Plus d\'amis à charger.', tag: 'FriendsProvider');
|
||||
} else {
|
||||
// Ajout des amis à la liste, en excluant l'utilisateur connecté
|
||||
for (final friend in newFriends) {
|
||||
if (friend.friendId != userId) {
|
||||
_friendsList.add(friend);
|
||||
_logger.i('[LOG] Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}');
|
||||
AppLogger.d('Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}', tag: 'FriendsProvider');
|
||||
} else {
|
||||
_logger.w("[WARN] L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}");
|
||||
AppLogger.w("L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}", tag: 'FriendsProvider');
|
||||
}
|
||||
}
|
||||
_currentPage++;
|
||||
_logger.i('[LOG] Préparation de la page suivante : $_currentPage');
|
||||
AppLogger.d('Préparation de la page suivante : $_currentPage', tag: 'FriendsProvider');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors du chargement des amis : $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du chargement des amis', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
_logger.i('[LOG] Fin du chargement des amis.');
|
||||
AppLogger.d('Fin du chargement des amis.', tag: 'FriendsProvider');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -115,12 +127,12 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// - Enlève l'ami de la liste locale.
|
||||
Future<void> removeFriend(String friendId) async {
|
||||
try {
|
||||
_logger.i('[LOG] Suppression de l\'ami avec l\'ID : $friendId');
|
||||
AppLogger.i('Suppression de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider');
|
||||
await friendsRepository.removeFriend(friendId); // Appel API pour supprimer l'ami
|
||||
_friendsList.removeWhere((friend) => friend.friendId == friendId); // Suppression locale
|
||||
_logger.i('[LOG] Ami supprimé localement avec succès : $friendId');
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de la suppression de l\'ami : $e');
|
||||
AppLogger.i('Ami supprimé localement avec succès : $friendId', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la suppression de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -134,18 +146,18 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// Retourne un `Future<Friend?>` contenant les détails de l'ami ou `null` en cas d'erreur.
|
||||
Future<Friend?> fetchFriendDetails(String userId, String friendId) async {
|
||||
try {
|
||||
_logger.i('[LOG] Récupération des détails de l\'ami avec l\'ID : $friendId');
|
||||
AppLogger.d('Récupération des détails de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider');
|
||||
final friendDetails = await friendsRepository.getFriendDetails(friendId, userId);
|
||||
|
||||
if (friendDetails != null) {
|
||||
_logger.i('[LOG] Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}');
|
||||
AppLogger.d('Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}', tag: 'FriendsProvider');
|
||||
} else {
|
||||
_logger.w('[WARN] Détails de l\'ami introuvables pour l\'ID : $friendId');
|
||||
AppLogger.w('Détails de l\'ami introuvables pour l\'ID : $friendId', tag: 'FriendsProvider');
|
||||
}
|
||||
|
||||
return friendDetails;
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de la récupération des détails de l\'ami : $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des détails de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -176,7 +188,7 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// Loggue l'action, met à jour le statut en local et appelle l'API pour mettre à jour le statut.
|
||||
Future<void> updateFriendStatus(String friendId, String status) async {
|
||||
try {
|
||||
_logger.i('[LOG] Mise à jour du statut de l\'ami avec l\'ID : $friendId');
|
||||
AppLogger.i('Mise à jour du statut de l\'ami avec l\'ID : $friendId', tag: 'FriendsProvider');
|
||||
|
||||
// Conversion du statut sous forme de chaîne en statut spécifique
|
||||
final friendStatus = _convertToFriendStatus(status);
|
||||
@@ -186,10 +198,10 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId);
|
||||
if (friendIndex != -1) {
|
||||
_friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus);
|
||||
_logger.i('[LOG] Statut de l\'ami mis à jour localement pour l\'ID : $friendId');
|
||||
AppLogger.i('Statut de l\'ami mis à jour localement pour l\'ID : $friendId', tag: 'FriendsProvider');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de la mise à jour du statut de l\'ami : $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la mise à jour du statut de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -211,14 +223,20 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
throw Exception('Utilisateur non connecté');
|
||||
}
|
||||
|
||||
_logger.i('[LOG] Ajout de l\'ami: userId=$currentUserId, friendId=$friendId');
|
||||
// VALIDATION: Empêcher l'utilisateur de s'ajouter lui-même comme ami
|
||||
if (currentUserId == friendId) {
|
||||
AppLogger.w('Tentative d\'ajout de soi-même comme ami bloquée', tag: 'FriendsProvider');
|
||||
throw Exception('Vous ne pouvez pas vous ajouter vous-même comme ami');
|
||||
}
|
||||
|
||||
AppLogger.i('Ajout de l\'ami: userId=$currentUserId, friendId=$friendId', tag: 'FriendsProvider');
|
||||
await friendsRepository.addFriend(currentUserId, friendId);
|
||||
_logger.i('[LOG] Demande d\'ami envoyée avec succès');
|
||||
|
||||
AppLogger.i('Demande d\'ami envoyée avec succès', tag: 'FriendsProvider');
|
||||
|
||||
// Rafraîchir la liste des amis après l'ajout
|
||||
// Note: L'ami ne sera visible qu'après acceptation de la demande
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de l\'ajout de l\'ami : $e');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de l\'ajout de l\'ami', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
rethrow; // Propager l'erreur pour que l'UI puisse l'afficher
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -231,7 +249,7 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
final secureStorage = SecureStorage();
|
||||
return await secureStorage.getUserId();
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de la récupération de l\'userId : $e');
|
||||
AppLogger.e('Erreur lors de la récupération de l\'userId', error: e, tag: 'FriendsProvider');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -264,6 +282,20 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
_requestsPerPage,
|
||||
);
|
||||
|
||||
// VALIDATION: Pour les demandes envoyées, currentUserId doit être l'expéditeur (userId)
|
||||
for (final request in requests) {
|
||||
if (request.userId != currentUserId) {
|
||||
AppLogger.e(
|
||||
'INCOHÉRENCE DÉTECTÉE dans fetchSentRequests: '
|
||||
'currentUserId=$currentUserId mais request.userId=${request.userId}, '
|
||||
'request.friendId=${request.friendId}, '
|
||||
'userFullName=${request.userFullName}, '
|
||||
'friendFullName=${request.friendFullName}',
|
||||
tag: 'FriendsProvider'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
_sentRequests.addAll(requests);
|
||||
_currentSentRequestPage = page;
|
||||
@@ -272,9 +304,9 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
_currentSentRequestPage = 0;
|
||||
}
|
||||
|
||||
_logger.i('[LOG] ${requests.length} demandes d\'amitié envoyées récupérées');
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de la récupération des demandes envoyées : $e');
|
||||
AppLogger.i('${requests.length} demandes d\'amitié envoyées récupérées', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des demandes envoyées', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoadingSentRequests = false;
|
||||
@@ -305,6 +337,20 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
_requestsPerPage,
|
||||
);
|
||||
|
||||
// VALIDATION: Pour les demandes reçues, currentUserId doit être le destinataire (friendId)
|
||||
for (final request in requests) {
|
||||
if (request.friendId != currentUserId) {
|
||||
AppLogger.e(
|
||||
'INCOHÉRENCE DÉTECTÉE dans fetchReceivedRequests: '
|
||||
'currentUserId=$currentUserId mais request.friendId=${request.friendId}, '
|
||||
'request.userId=${request.userId}, '
|
||||
'userFullName=${request.userFullName}, '
|
||||
'friendFullName=${request.friendFullName}',
|
||||
tag: 'FriendsProvider'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
_receivedRequests.addAll(requests);
|
||||
_currentReceivedRequestPage = page;
|
||||
@@ -313,9 +359,9 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
_currentReceivedRequestPage = 0;
|
||||
}
|
||||
|
||||
_logger.i('[LOG] ${requests.length} demandes d\'amitié reçues récupérées');
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de la récupération des demandes reçues : $e');
|
||||
AppLogger.i('${requests.length} demandes d\'amitié reçues récupérées', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des demandes reçues', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoadingReceivedRequests = false;
|
||||
@@ -326,7 +372,7 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// Accepte une demande d'amitié.
|
||||
Future<void> acceptFriendRequest(String friendshipId) async {
|
||||
try {
|
||||
_logger.i('[LOG] Acceptation de la demande d\'amitié: $friendshipId');
|
||||
AppLogger.i('Acceptation de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider');
|
||||
await friendsRepository.acceptFriendRequest(friendshipId);
|
||||
|
||||
// Retirer la demande de la liste des demandes reçues
|
||||
@@ -338,9 +384,9 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
await fetchFriends(currentUserId);
|
||||
}
|
||||
|
||||
_logger.i('[LOG] Demande d\'amitié acceptée avec succès');
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de l\'acceptation de la demande : $e');
|
||||
AppLogger.i('Demande d\'amitié acceptée avec succès', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de l\'acceptation de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -350,15 +396,15 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// Rejette une demande d'amitié.
|
||||
Future<void> rejectFriendRequest(String friendshipId) async {
|
||||
try {
|
||||
_logger.i('[LOG] Rejet de la demande d\'amitié: $friendshipId');
|
||||
AppLogger.i('Rejet de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider');
|
||||
await friendsRepository.rejectFriendRequest(friendshipId);
|
||||
|
||||
// Retirer la demande de la liste des demandes reçues
|
||||
_receivedRequests.removeWhere((req) => req.friendshipId == friendshipId);
|
||||
|
||||
_logger.i('[LOG] Demande d\'amitié rejetée avec succès');
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors du rejet de la demande : $e');
|
||||
AppLogger.i('Demande d\'amitié rejetée avec succès', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du rejet de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
@@ -368,18 +414,142 @@ class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par
|
||||
/// Annule une demande d'amitié envoyée.
|
||||
Future<void> cancelFriendRequest(String friendshipId) async {
|
||||
try {
|
||||
_logger.i('[LOG] Annulation de la demande d\'amitié: $friendshipId');
|
||||
AppLogger.i('Annulation de la demande d\'amitié: $friendshipId', tag: 'FriendsProvider');
|
||||
await friendsRepository.cancelFriendRequest(friendshipId);
|
||||
|
||||
|
||||
// Retirer la demande de la liste des demandes envoyées
|
||||
_sentRequests.removeWhere((req) => req.friendshipId == friendshipId);
|
||||
|
||||
_logger.i('[LOG] Demande d\'amitié annulée avec succès');
|
||||
} catch (e) {
|
||||
_logger.e('[ERROR] Erreur lors de l\'annulation de la demande : $e');
|
||||
|
||||
AppLogger.i('Demande d\'amitié annulée avec succès', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de l\'annulation de la demande', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les suggestions d'amis pour l'utilisateur actuel.
|
||||
///
|
||||
/// Les suggestions sont basées sur les amis en commun et d'autres critères
|
||||
/// de pertinence définis par le backend.
|
||||
///
|
||||
/// [limit] : Nombre maximum de suggestions à récupérer (par défaut 10).
|
||||
Future<void> fetchFriendSuggestions({int limit = 10}) async {
|
||||
try {
|
||||
final currentUserId = await _getCurrentUserId();
|
||||
if (currentUserId == null || currentUserId.isEmpty) {
|
||||
throw Exception('Utilisateur non connecté');
|
||||
}
|
||||
|
||||
_isLoadingSuggestions = true;
|
||||
notifyListeners();
|
||||
|
||||
AppLogger.i('Récupération des suggestions d\'amis (limit: $limit)', tag: 'FriendsProvider');
|
||||
_friendSuggestions = await friendsRepository.getFriendSuggestions(currentUserId, limit: limit);
|
||||
|
||||
AppLogger.i('${_friendSuggestions.length} suggestions d\'amis récupérées', tag: 'FriendsProvider');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la récupération des suggestions', error: e, stackTrace: stackTrace, tag: 'FriendsProvider');
|
||||
_friendSuggestions = [];
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoadingSuggestions = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Connecte le service de notifications temps réel.
|
||||
///
|
||||
/// Cette méthode doit être appelée après la connexion de l'utilisateur pour
|
||||
/// recevoir les notifications de demandes d'amitié en temps réel.
|
||||
///
|
||||
/// [service] : Le service de notifications temps réel à connecter.
|
||||
void connectRealtime(RealtimeNotificationService service) {
|
||||
_realtimeService = service;
|
||||
|
||||
// Écouter les demandes d'amitié en temps réel
|
||||
_friendRequestSubscription = service.friendRequestStream.listen(
|
||||
_handleFriendRequestNotification,
|
||||
onError: (error) {
|
||||
AppLogger.e('Erreur dans le stream de demandes d\'amitié', error: error, tag: 'FriendsProvider');
|
||||
},
|
||||
);
|
||||
|
||||
AppLogger.i('Service de notifications temps réel connecté pour les demandes d\'amitié', tag: 'FriendsProvider');
|
||||
}
|
||||
|
||||
/// Gère les notifications de demandes d'amitié reçues en temps réel.
|
||||
///
|
||||
/// Cette méthode est appelée automatiquement lorsqu'une notification
|
||||
/// est reçue via WebSocket.
|
||||
void _handleFriendRequestNotification(FriendRequestNotification notification) {
|
||||
AppLogger.i('Notification de demande d\'amitié reçue: ${notification.type}', tag: 'FriendsProvider');
|
||||
|
||||
switch (notification.type) {
|
||||
case 'received':
|
||||
// Rafraîchir les demandes reçues pour inclure la nouvelle demande
|
||||
_refreshReceivedRequests();
|
||||
AppLogger.i('Nouvelle demande d\'amitié de ${notification.senderName}', tag: 'FriendsProvider');
|
||||
break;
|
||||
|
||||
case 'accepted':
|
||||
// Rafraîchir la liste d'amis pour inclure le nouvel ami
|
||||
_refreshFriendsList();
|
||||
|
||||
// Supprimer de la liste des demandes envoyées si présente
|
||||
_sentRequests.removeWhere((request) => request.friendshipId == notification.requestId);
|
||||
notifyListeners();
|
||||
|
||||
AppLogger.i('${notification.senderName} a accepté votre demande', tag: 'FriendsProvider');
|
||||
break;
|
||||
|
||||
case 'rejected':
|
||||
// Supprimer de la liste des demandes envoyées
|
||||
_sentRequests.removeWhere((request) => request.friendshipId == notification.requestId);
|
||||
notifyListeners();
|
||||
|
||||
AppLogger.i('Demande d\'amitié rejetée: ${notification.requestId}', tag: 'FriendsProvider');
|
||||
break;
|
||||
|
||||
default:
|
||||
AppLogger.w('Type de notification inconnu: ${notification.type}', tag: 'FriendsProvider');
|
||||
}
|
||||
}
|
||||
|
||||
/// Rafraîchit la liste des demandes reçues en arrière-plan.
|
||||
Future<void> _refreshReceivedRequests() async {
|
||||
try {
|
||||
await fetchReceivedRequests(loadMore: false);
|
||||
} catch (e) {
|
||||
AppLogger.e('Erreur lors du rafraîchissement des demandes reçues', error: e, tag: 'FriendsProvider');
|
||||
}
|
||||
}
|
||||
|
||||
/// Rafraîchit la liste d'amis en arrière-plan.
|
||||
Future<void> _refreshFriendsList() async {
|
||||
try {
|
||||
final currentUserId = await _getCurrentUserId();
|
||||
if (currentUserId == null || currentUserId.isEmpty) return;
|
||||
|
||||
await fetchFriends(currentUserId, loadMore: false);
|
||||
} catch (e) {
|
||||
AppLogger.e('Erreur lors du rafraîchissement de la liste d\'amis', error: e, tag: 'FriendsProvider');
|
||||
}
|
||||
}
|
||||
|
||||
/// Déconnecte le service de notifications temps réel.
|
||||
void disconnectRealtime() {
|
||||
_friendRequestSubscription?.cancel();
|
||||
_friendRequestSubscription = null;
|
||||
_realtimeService = null;
|
||||
|
||||
AppLogger.i('Service de notifications temps réel déconnecté', tag: 'FriendsProvider');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnectRealtime();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
79
lib/data/providers/presence_provider.dart
Normal file
79
lib/data/providers/presence_provider.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../services/realtime_notification_service.dart';
|
||||
|
||||
/// Provider pour gérer la présence utilisateur (online/offline).
|
||||
///
|
||||
/// Ce provider :
|
||||
/// - Écoute les mises à jour de présence via WebSocket
|
||||
/// - Maintient une map userId -> isOnline
|
||||
/// - Envoie un heartbeat toutes les 25 secondes pour maintenir le statut online
|
||||
/// - Notifie les widgets qui écoutent lors des changements de présence
|
||||
class PresenceProvider extends ChangeNotifier {
|
||||
PresenceProvider();
|
||||
|
||||
// Map userId -> isOnline
|
||||
final Map<String, bool> _presenceMap = {};
|
||||
|
||||
RealtimeNotificationService? _realtimeService;
|
||||
StreamSubscription<PresenceUpdate>? _presenceSubscription;
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
/// Obtenir le statut online d'un utilisateur.
|
||||
bool isUserOnline(String userId) {
|
||||
return _presenceMap[userId] ?? false;
|
||||
}
|
||||
|
||||
/// Connecter au service temps réel et démarrer heartbeat.
|
||||
void connectRealtime(RealtimeNotificationService service) {
|
||||
_realtimeService = service;
|
||||
|
||||
// Écouter les mises à jour de présence
|
||||
_presenceSubscription = service.presenceStream.listen((update) {
|
||||
_presenceMap[update.userId] = update.isOnline;
|
||||
notifyListeners();
|
||||
AppLogger.i(
|
||||
'Présence mise à jour: ${update.userId} -> ${update.isOnline}',
|
||||
tag: 'PresenceProvider',
|
||||
);
|
||||
});
|
||||
|
||||
// Démarrer heartbeat (toutes les 25 secondes)
|
||||
_startHeartbeat();
|
||||
|
||||
AppLogger.i('PresenceProvider connecté', tag: 'PresenceProvider');
|
||||
}
|
||||
|
||||
/// Démarrer le heartbeat pour maintenir le statut online.
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(
|
||||
const Duration(seconds: 25),
|
||||
(_) {
|
||||
_realtimeService?.sendHeartbeat();
|
||||
AppLogger.d('Heartbeat envoyé', tag: 'PresenceProvider');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Déconnecter et arrêter le heartbeat.
|
||||
void disconnectRealtime() {
|
||||
_presenceSubscription?.cancel();
|
||||
_presenceSubscription = null;
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
_realtimeService = null;
|
||||
_presenceMap.clear();
|
||||
notifyListeners();
|
||||
AppLogger.i('PresenceProvider déconnecté', tag: 'PresenceProvider');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnectRealtime();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,6 @@ class UserProvider with ChangeNotifier {
|
||||
email: '',
|
||||
motDePasse: '',
|
||||
profileImageUrl: '',
|
||||
eventsCount: 0,
|
||||
friendsCount: 0,
|
||||
postsCount: 0,
|
||||
visitedPlacesCount: 0,
|
||||
);
|
||||
|
||||
bool _isEmailDisplayedElsewhere = false; // Ajout de la propriété pour contrôler l'affichage de l'email
|
||||
@@ -28,7 +24,7 @@ class UserProvider with ChangeNotifier {
|
||||
/// Méthode pour définir l'état d'affichage de l'email.
|
||||
void setEmailDisplayedElsewhere(bool value) {
|
||||
_isEmailDisplayedElsewhere = value;
|
||||
debugPrint("[LOG] isEmailDisplayedElsewhere mis à jour : $_isEmailDisplayedElsewhere");
|
||||
debugPrint('[LOG] isEmailDisplayedElsewhere mis à jour : $_isEmailDisplayedElsewhere');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -37,7 +33,7 @@ class UserProvider with ChangeNotifier {
|
||||
void setUser(User user) {
|
||||
debugPrint("[LOG] Tentative de définition des informations de l'utilisateur : ${user.toString()}");
|
||||
_user = user;
|
||||
debugPrint("[LOG] Informations utilisateur définies : ${_user.toString()}");
|
||||
debugPrint('[LOG] Informations utilisateur définies : ${_user.toString()}');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -48,7 +44,7 @@ class UserProvider with ChangeNotifier {
|
||||
int? postsCount,
|
||||
int? visitedPlacesCount,
|
||||
}) {
|
||||
debugPrint("[LOG] Mise à jour des statistiques utilisateur");
|
||||
debugPrint('[LOG] Mise à jour des statistiques utilisateur');
|
||||
|
||||
_user = User(
|
||||
userId: _user.userId,
|
||||
@@ -63,14 +59,14 @@ class UserProvider with ChangeNotifier {
|
||||
visitedPlacesCount: visitedPlacesCount ?? _user.visitedPlacesCount,
|
||||
);
|
||||
|
||||
debugPrint("[LOG] Nouvelles statistiques utilisateur : ${_user.toString()}");
|
||||
debugPrint('[LOG] Nouvelles statistiques utilisateur : ${_user.toString()}');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Méthode pour réinitialiser les informations de l'utilisateur.
|
||||
void resetUser() {
|
||||
debugPrint("[LOG] Réinitialisation des informations de l'utilisateur.");
|
||||
debugPrint("[LOG] Valeurs avant réinitialisation : ${_user.toString()}");
|
||||
debugPrint('[LOG] Valeurs avant réinitialisation : ${_user.toString()}');
|
||||
|
||||
_user = const User(
|
||||
userId: '',
|
||||
@@ -79,13 +75,9 @@ class UserProvider with ChangeNotifier {
|
||||
email: '',
|
||||
motDePasse: '',
|
||||
profileImageUrl: '',
|
||||
eventsCount: 0,
|
||||
friendsCount: 0,
|
||||
postsCount: 0,
|
||||
visitedPlacesCount: 0,
|
||||
);
|
||||
|
||||
debugPrint("[LOG] Informations utilisateur réinitialisées : ${_user.toString()}");
|
||||
debugPrint('[LOG] Informations utilisateur réinitialisées : ${_user.toString()}');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
405
lib/data/repositories/chat_repository_impl.dart
Normal file
405
lib/data/repositories/chat_repository_impl.dart
Normal file
@@ -0,0 +1,405 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/errors/failures.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../domain/entities/chat_message.dart';
|
||||
import '../../domain/entities/conversation.dart';
|
||||
import '../../domain/repositories/chat_repository.dart';
|
||||
import '../datasources/chat_remote_data_source.dart';
|
||||
|
||||
/// Implémentation du repository de chat.
|
||||
///
|
||||
/// Cette classe fait le lien entre la couche domaine et la couche données,
|
||||
/// en convertissant les exceptions en failures et en gérant la transformation
|
||||
/// entre les modèles de données et les entités de domaine.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final repository = ChatRepositoryImpl(
|
||||
/// remoteDataSource: chatRemoteDataSource,
|
||||
/// );
|
||||
/// final result = await repository.getConversations('userId');
|
||||
/// result.fold(
|
||||
/// (failure) => print('Erreur: $failure'),
|
||||
/// (conversations) => print('${conversations.length} conversations'),
|
||||
/// );
|
||||
/// ```
|
||||
class ChatRepositoryImpl implements ChatRepository {
|
||||
/// Crée une nouvelle instance de [ChatRepositoryImpl].
|
||||
///
|
||||
/// [remoteDataSource] La source de données distante pour le chat
|
||||
ChatRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
/// Source de données distante pour les opérations de chat
|
||||
final ChatRemoteDataSource remoteDataSource;
|
||||
|
||||
/// Log un message si le mode debug est activé.
|
||||
void _log(String message) {
|
||||
AppLogger.d(message, tag: 'ChatRepositoryImpl');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Conversation>>> getConversations(
|
||||
String userId,
|
||||
) async {
|
||||
_log('Récupération des conversations pour $userId');
|
||||
|
||||
try {
|
||||
if (userId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID utilisateur ne peut pas être vide',
|
||||
field: 'userId',
|
||||
));
|
||||
}
|
||||
|
||||
final conversationModels = await remoteDataSource.getConversations(userId);
|
||||
final conversations = conversationModels.map((model) => model.toEntity()).toList();
|
||||
_log('${conversations.length} conversations récupérées');
|
||||
return Right(conversations);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la récupération des conversations: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Conversation>> getOrCreateConversation(
|
||||
String userId,
|
||||
String participantId,
|
||||
) async {
|
||||
_log('Récupération/création conversation entre $userId et $participantId');
|
||||
|
||||
try {
|
||||
if (userId.isEmpty || participantId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'Les IDs utilisateur et participant sont requis',
|
||||
));
|
||||
}
|
||||
|
||||
if (userId == participantId) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'Impossible de créer une conversation avec soi-même',
|
||||
));
|
||||
}
|
||||
|
||||
final conversationModel = await remoteDataSource.getOrCreateConversation(
|
||||
userId,
|
||||
participantId,
|
||||
);
|
||||
_log('Conversation récupérée/créée avec succès');
|
||||
return Right(conversationModel.toEntity());
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la récupération/création de la conversation: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<ChatMessage>>> getMessages(
|
||||
String conversationId, {
|
||||
int page = 0,
|
||||
int size = 50,
|
||||
}) async {
|
||||
_log('Récupération des messages de $conversationId (page: $page)');
|
||||
|
||||
try {
|
||||
if (conversationId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID de conversation ne peut pas être vide',
|
||||
field: 'conversationId',
|
||||
));
|
||||
}
|
||||
|
||||
final messageModels = await remoteDataSource.getMessages(
|
||||
conversationId,
|
||||
page: page,
|
||||
size: size,
|
||||
);
|
||||
final messages = messageModels.map((model) => model.toEntity()).toList();
|
||||
_log('${messages.length} messages récupérés');
|
||||
return Right(messages);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la récupération des messages: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, ChatMessage>> sendMessage({
|
||||
required String senderId,
|
||||
required String recipientId,
|
||||
required String content,
|
||||
String? messageType,
|
||||
String? mediaUrl,
|
||||
}) async {
|
||||
_log('Envoi d\'un message de $senderId à $recipientId');
|
||||
|
||||
try {
|
||||
if (senderId.isEmpty || recipientId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'Les IDs expéditeur et destinataire sont requis',
|
||||
));
|
||||
}
|
||||
|
||||
if (content.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'Le contenu du message ne peut pas être vide',
|
||||
field: 'content',
|
||||
));
|
||||
}
|
||||
|
||||
if (senderId == recipientId) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'Impossible d\'envoyer un message à soi-même',
|
||||
));
|
||||
}
|
||||
|
||||
final messageModel = await remoteDataSource.sendMessage(
|
||||
senderId: senderId,
|
||||
recipientId: recipientId,
|
||||
content: content,
|
||||
messageType: messageType,
|
||||
mediaUrl: mediaUrl,
|
||||
);
|
||||
_log('Message envoyé avec succès');
|
||||
return Right(messageModel.toEntity());
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de l\'envoi du message: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> markMessageAsRead(String messageId) async {
|
||||
_log('Marquage du message $messageId comme lu');
|
||||
|
||||
try {
|
||||
if (messageId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID du message ne peut pas être vide',
|
||||
field: 'messageId',
|
||||
));
|
||||
}
|
||||
|
||||
await remoteDataSource.markMessageAsRead(messageId);
|
||||
_log('Message marqué comme lu');
|
||||
return const Right(null);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors du marquage du message: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> markConversationAsRead(
|
||||
String conversationId,
|
||||
String userId,
|
||||
) async {
|
||||
_log('Marquage de tous les messages de $conversationId comme lus');
|
||||
|
||||
try {
|
||||
if (conversationId.isEmpty || userId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'Les IDs conversation et utilisateur sont requis',
|
||||
));
|
||||
}
|
||||
|
||||
await remoteDataSource.markConversationAsRead(conversationId, userId);
|
||||
_log('Conversation marquée comme lue');
|
||||
return const Right(null);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors du marquage de la conversation: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteMessage(String messageId) async {
|
||||
_log('Suppression du message $messageId');
|
||||
|
||||
try {
|
||||
if (messageId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID du message ne peut pas être vide',
|
||||
field: 'messageId',
|
||||
));
|
||||
}
|
||||
|
||||
await remoteDataSource.deleteMessage(messageId);
|
||||
_log('Message supprimé');
|
||||
return const Right(null);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la suppression du message: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteConversation(String conversationId) async {
|
||||
_log('Suppression de la conversation $conversationId');
|
||||
|
||||
try {
|
||||
if (conversationId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID de la conversation ne peut pas être vide',
|
||||
field: 'conversationId',
|
||||
));
|
||||
}
|
||||
|
||||
await remoteDataSource.deleteConversation(conversationId);
|
||||
_log('Conversation supprimée');
|
||||
return const Right(null);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la suppression de la conversation: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, int>> getUnreadMessagesCount(String userId) async {
|
||||
_log('Récupération du nombre de messages non lus pour $userId');
|
||||
|
||||
try {
|
||||
if (userId.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID utilisateur ne peut pas être vide',
|
||||
field: 'userId',
|
||||
));
|
||||
}
|
||||
|
||||
final count = await remoteDataSource.getUnreadMessagesCount(userId);
|
||||
_log('$count messages non lus');
|
||||
return Right(count);
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Non autorisé: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la récupération du nombre de messages non lus: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,4 +95,13 @@ abstract class FriendsRepository {
|
||||
///
|
||||
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
||||
Future<void> cancelFriendRequest(String friendshipId);
|
||||
|
||||
/// Récupère les suggestions d'amis pour un utilisateur.
|
||||
///
|
||||
/// [userId] : Identifiant unique de l'utilisateur.
|
||||
/// [limit] : Nombre maximum de suggestions à retourner (par défaut 10).
|
||||
///
|
||||
/// Retourne une liste de suggestions d'amis basées sur les amis en commun
|
||||
/// et d'autres critères de pertinence.
|
||||
Future<List<dynamic>> getFriendSuggestions(String userId, {int limit = 10});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:http/http.dart' as http;
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../domain/entities/friend.dart';
|
||||
import '../../domain/entities/friend_request.dart';
|
||||
import 'friends_repository.dart';
|
||||
@@ -221,9 +222,7 @@ class FriendsRepositoryImpl implements FriendsRepository {
|
||||
|
||||
/// Log un message si le mode debug est activé.
|
||||
void _log(String message) {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
print('[FriendsRepositoryImpl] $message');
|
||||
}
|
||||
AppLogger.d(message, tag: 'FriendsRepositoryImpl');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -683,4 +682,56 @@ class FriendsRepositoryImpl implements FriendsRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les suggestions d'amis pour un utilisateur.
|
||||
///
|
||||
/// [userId] L'identifiant unique de l'utilisateur
|
||||
/// [limit] Nombre maximum de suggestions (par défaut 10)
|
||||
///
|
||||
/// Returns une liste d'objets [FriendSuggestion]
|
||||
///
|
||||
/// Throws [ServerException] en cas d'erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final suggestions = await repository.getFriendSuggestions('user123', limit: 5);
|
||||
/// ```
|
||||
@override
|
||||
Future<List<dynamic>> getFriendSuggestions(String userId, {int limit = 10}) async {
|
||||
_log('Récupération des suggestions d\'amis pour l\'utilisateur $userId (limit: $limit)');
|
||||
|
||||
if (userId.isEmpty) {
|
||||
throw ValidationException('L\'ID utilisateur ne peut pas être vide');
|
||||
}
|
||||
|
||||
if (limit <= 0) {
|
||||
throw ValidationException('La limite doit être > 0');
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(Urls.getFriendSuggestionsWithUserId(userId, limit: limit));
|
||||
final response = await _performRequest('GET', uri);
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
_log('Aucune suggestion trouvée (404) - retour d\'une liste vide');
|
||||
return [];
|
||||
}
|
||||
|
||||
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>?;
|
||||
|
||||
if (jsonResponse == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final suggestions = jsonResponse
|
||||
.map((json) => json as Map<String, dynamic>)
|
||||
.toList();
|
||||
|
||||
_log('${suggestions.length} suggestions d\'amis récupérées avec succès');
|
||||
return suggestions;
|
||||
} catch (e) {
|
||||
_log('Erreur lors de la récupération des suggestions d\'amis: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,308 @@
|
||||
import 'dart:convert';
|
||||
import 'package:afterwork/data/datasources/user_remote_data_source.dart';
|
||||
import 'package:afterwork/data/models/user_model.dart';
|
||||
import 'package:afterwork/domain/entities/user.dart';
|
||||
import 'package:afterwork/domain/repositories/user_repository.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
import '../../core/errors/failures.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../domain/entities/user.dart';
|
||||
import '../../domain/repositories/user_repository.dart';
|
||||
import '../datasources/user_remote_data_source.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// Implémentation du repository des utilisateurs.
|
||||
/// Cette classe fait le lien entre les appels de l'application et les services distants pour les opérations sur les utilisateurs.
|
||||
///
|
||||
/// Cette classe fait le lien entre la couche domaine et la couche données,
|
||||
/// en convertissant les exceptions en failures et en gérant la transformation
|
||||
/// entre les modèles de données et les entités de domaine.
|
||||
///
|
||||
/// **Usage:**
|
||||
/// ```dart
|
||||
/// final repository = UserRepositoryImpl(
|
||||
/// remoteDataSource: userRemoteDataSource,
|
||||
/// );
|
||||
/// final result = await repository.getUser('user123');
|
||||
/// result.fold(
|
||||
/// (failure) => print('Erreur: $failure'),
|
||||
/// (user) => print('Utilisateur: $user'),
|
||||
/// );
|
||||
/// ```
|
||||
class UserRepositoryImpl implements UserRepository {
|
||||
final UserRemoteDataSource remoteDataSource;
|
||||
|
||||
/// Constructeur avec injection de la source de données distante.
|
||||
/// Crée une nouvelle instance de [UserRepositoryImpl].
|
||||
///
|
||||
/// [remoteDataSource] La source de données distante pour les utilisateurs
|
||||
UserRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
/// Récupère un utilisateur par son ID depuis la source de données distante.
|
||||
@override
|
||||
Future<User> getUser(String id) async {
|
||||
UserModel userModel = await remoteDataSource.getUser(id);
|
||||
return userModel; // Retourne un UserModel qui est un sous-type de User.
|
||||
/// Source de données distante pour les opérations sur les utilisateurs
|
||||
final UserRemoteDataSource remoteDataSource;
|
||||
|
||||
/// Log un message si le mode debug est activé.
|
||||
void _log(String message) {
|
||||
AppLogger.d(message, tag: 'UserRepositoryImpl');
|
||||
}
|
||||
|
||||
/// Authentifie un utilisateur par email et mot de passe (en clair, temporairement).
|
||||
Future<UserModel> authenticateUser(String email, String password) async {
|
||||
print("Tentative d'authentification pour l'email : $email");
|
||||
/// Récupère un utilisateur par son ID depuis la source de données distante.
|
||||
///
|
||||
/// [id] L'identifiant de l'utilisateur
|
||||
///
|
||||
/// Returns [Right] avec l'utilisateur si succès, [Left] avec une [Failure] si erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final result = await repository.getUser('user123');
|
||||
/// result.fold(
|
||||
/// (failure) => handleError(failure),
|
||||
/// (user) => displayUser(user),
|
||||
/// );
|
||||
/// ```
|
||||
@override
|
||||
Future<Either<Failure, User>> getUser(String id) async {
|
||||
_log('Récupération de l\'utilisateur $id');
|
||||
|
||||
try {
|
||||
// Requête POST avec les identifiants utilisateur pour l'authentification
|
||||
final response = await http.post(
|
||||
Uri.parse('${Urls.baseUrl}/users/authenticate'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'email': email, 'motDePasse': password}),
|
||||
if (id.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID utilisateur ne peut pas être vide',
|
||||
field: 'id',
|
||||
));
|
||||
}
|
||||
|
||||
final userModel = await remoteDataSource.getUser(id);
|
||||
_log('Utilisateur $id récupéré avec succès');
|
||||
return Right(userModel.toEntity());
|
||||
} on UserNotFoundException catch (e) {
|
||||
_log('Utilisateur $id non trouvé: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
code: 'USER_NOT_FOUND',
|
||||
statusCode: 404,
|
||||
));
|
||||
} on ValidationException catch (e) {
|
||||
_log('Erreur de validation: ${e.message}');
|
||||
return Left(ValidationFailure(message:
|
||||
e.message,
|
||||
field: e.field,
|
||||
));
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la récupération de l\'utilisateur: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Authentifie un utilisateur avec son email et mot de passe.
|
||||
///
|
||||
/// [email] L'adresse email de l'utilisateur
|
||||
/// [password] Le mot de passe de l'utilisateur
|
||||
///
|
||||
/// Returns [Right] avec l'utilisateur authentifié si succès,
|
||||
/// [Left] avec une [Failure] si erreur
|
||||
///
|
||||
/// **Exemple:**
|
||||
/// ```dart
|
||||
/// final result = await repository.authenticateUser(
|
||||
/// 'user@example.com',
|
||||
/// 'password123',
|
||||
/// );
|
||||
/// ```
|
||||
Future<Either<Failure, User>> authenticateUser(
|
||||
String email,
|
||||
String password,
|
||||
) async {
|
||||
_log('Authentification pour: $email');
|
||||
|
||||
try {
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'email et le mot de passe sont requis',
|
||||
));
|
||||
}
|
||||
|
||||
final userModel = await remoteDataSource.authenticateUser(email, password);
|
||||
_log('Authentification réussie pour: ${userModel.email}');
|
||||
return Right(userModel.toEntity());
|
||||
} on UnauthorizedException catch (e) {
|
||||
_log('Authentification échouée: ${e.message}');
|
||||
return Left(AuthenticationFailure(
|
||||
message: e.message,
|
||||
code: 'UNAUTHORIZED',
|
||||
));
|
||||
} on ValidationException catch (e) {
|
||||
_log('Erreur de validation: ${e.message}');
|
||||
return Left(ValidationFailure(message:
|
||||
e.message,
|
||||
field: e.field,
|
||||
));
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de l\'authentification: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouvel utilisateur.
|
||||
///
|
||||
/// [user] L'entité utilisateur à créer
|
||||
///
|
||||
/// Returns [Right] avec l'utilisateur créé si succès,
|
||||
/// [Left] avec une [Failure] si erreur
|
||||
Future<Either<Failure, User>> createUser(User user) async {
|
||||
_log('Création d\'un nouvel utilisateur: ${user.email}');
|
||||
|
||||
try {
|
||||
final userModel = UserModel(
|
||||
userId: user.userId,
|
||||
userLastName: user.userLastName,
|
||||
userFirstName: user.userFirstName,
|
||||
email: user.email,
|
||||
motDePasse: user.motDePasse,
|
||||
profileImageUrl: user.profileImageUrl,
|
||||
eventsCount: user.eventsCount,
|
||||
friendsCount: user.friendsCount,
|
||||
postsCount: user.postsCount,
|
||||
visitedPlacesCount: user.visitedPlacesCount,
|
||||
);
|
||||
|
||||
print("Réponse du serveur pour l'authentification : ${response.statusCode} - ${response.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return UserModel.fromJson(jsonDecode(response.body));
|
||||
} else {
|
||||
throw Exception("Erreur lors de l'authentification : ${response.statusCode}");
|
||||
}
|
||||
final createdUser = await remoteDataSource.createUser(userModel);
|
||||
_log('Utilisateur créé avec succès: ${createdUser.userId}');
|
||||
return Right(createdUser.toEntity());
|
||||
} on ConflictException catch (e) {
|
||||
_log('Conflit lors de la création: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
code: 'CONFLICT',
|
||||
statusCode: 409,
|
||||
));
|
||||
} on ValidationException catch (e) {
|
||||
_log('Erreur de validation: ${e.message}');
|
||||
return Left(ValidationFailure(message:
|
||||
e.message,
|
||||
field: e.field,
|
||||
));
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} catch (e) {
|
||||
print("Erreur d'authentification : $e");
|
||||
throw Exception("Erreur lors de l'authentification : $e");
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la création de l\'utilisateur: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour un utilisateur existant.
|
||||
///
|
||||
/// [user] L'entité utilisateur avec les nouvelles données
|
||||
///
|
||||
/// Returns [Right] avec l'utilisateur mis à jour si succès,
|
||||
/// [Left] avec une [Failure] si erreur
|
||||
Future<Either<Failure, User>> updateUser(User user) async {
|
||||
_log('Mise à jour de l\'utilisateur ${user.userId}');
|
||||
|
||||
try {
|
||||
final userModel = UserModel(
|
||||
userId: user.userId,
|
||||
userLastName: user.userLastName,
|
||||
userFirstName: user.userFirstName,
|
||||
email: user.email,
|
||||
motDePasse: user.motDePasse,
|
||||
profileImageUrl: user.profileImageUrl,
|
||||
eventsCount: user.eventsCount,
|
||||
friendsCount: user.friendsCount,
|
||||
postsCount: user.postsCount,
|
||||
visitedPlacesCount: user.visitedPlacesCount,
|
||||
);
|
||||
|
||||
final updatedUser = await remoteDataSource.updateUser(userModel);
|
||||
_log('Utilisateur ${user.userId} mis à jour avec succès');
|
||||
return Right(updatedUser.toEntity());
|
||||
} on UserNotFoundException catch (e) {
|
||||
_log('Utilisateur non trouvé: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
code: 'USER_NOT_FOUND',
|
||||
statusCode: 404,
|
||||
));
|
||||
} on ValidationException catch (e) {
|
||||
_log('Erreur de validation: ${e.message}');
|
||||
return Left(ValidationFailure(message:
|
||||
e.message,
|
||||
field: e.field,
|
||||
));
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la mise à jour de l\'utilisateur: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime un utilisateur.
|
||||
///
|
||||
/// [id] L'identifiant de l'utilisateur à supprimer
|
||||
///
|
||||
/// Returns [Right] avec `null` si succès, [Left] avec une [Failure] si erreur
|
||||
Future<Either<Failure, void>> deleteUser(String id) async {
|
||||
_log('Suppression de l\'utilisateur $id');
|
||||
|
||||
try {
|
||||
if (id.isEmpty) {
|
||||
return const Left(ValidationFailure(message:
|
||||
'L\'ID utilisateur ne peut pas être vide',
|
||||
field: 'id',
|
||||
));
|
||||
}
|
||||
|
||||
await remoteDataSource.deleteUser(id);
|
||||
_log('Utilisateur $id supprimé avec succès');
|
||||
return const Right(null);
|
||||
} on UserNotFoundException catch (e) {
|
||||
_log('Utilisateur non trouvé: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
code: 'USER_NOT_FOUND',
|
||||
statusCode: 404,
|
||||
));
|
||||
} on ValidationException catch (e) {
|
||||
_log('Erreur de validation: ${e.message}');
|
||||
return Left(ValidationFailure(message:
|
||||
e.message,
|
||||
field: e.field,
|
||||
));
|
||||
} on ServerException catch (e) {
|
||||
_log('Erreur serveur: ${e.message}');
|
||||
return Left(ServerFailure(
|
||||
message: e.message,
|
||||
statusCode: e.statusCode,
|
||||
));
|
||||
} catch (e) {
|
||||
_log('Erreur inattendue: $e');
|
||||
return Left(ServerFailure(
|
||||
message: 'Erreur lors de la suppression de l\'utilisateur: $e',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// Service pour gérer le chargement des catégories depuis un fichier JSON.
|
||||
class CategoryService {
|
||||
/// Méthode pour charger les catégories depuis un fichier JSON.
|
||||
@@ -9,23 +11,25 @@ class CategoryService {
|
||||
Future<Map<String, List<String>>> loadCategories() async {
|
||||
try {
|
||||
// Charger le fichier JSON à partir des assets
|
||||
print('Chargement du fichier JSON des catégories...');
|
||||
AppLogger.d('Chargement du fichier JSON des catégories...', tag: 'CategoryService');
|
||||
final String response = await rootBundle.loadString('lib/assets/json/event_categories.json');
|
||||
|
||||
// Décoder le contenu du fichier JSON
|
||||
final Map<String, dynamic> data = json.decode(response);
|
||||
print('Données JSON décodées avec succès.');
|
||||
final dynamic decodedData = json.decode(response);
|
||||
final Map<String, dynamic> data = decodedData as Map<String, dynamic>;
|
||||
AppLogger.d('Données JSON décodées avec succès.', tag: 'CategoryService');
|
||||
|
||||
// Transformer les données en un Map de catégories par type
|
||||
final Map<String, List<String>> categoriesByType = (data['categories'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, List<String>.from(value)),
|
||||
final categoriesData = data['categories'] as Map<String, dynamic>;
|
||||
final Map<String, List<String>> categoriesByType = categoriesData.map(
|
||||
(key, value) => MapEntry(key, List<String>.from(value as List)),
|
||||
);
|
||||
|
||||
print('Catégories chargées: $categoriesByType');
|
||||
AppLogger.d('Catégories chargées: ${categoriesByType.keys.length} types', tag: 'CategoryService');
|
||||
return categoriesByType;
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
// Gérer les erreurs de chargement ou de décodage
|
||||
print('Erreur lors du chargement des catégories: $e');
|
||||
AppLogger.e('Erreur lors du chargement des catégories', error: e, stackTrace: stackTrace, tag: 'CategoryService');
|
||||
throw Exception('Impossible de charger les catégories.');
|
||||
}
|
||||
}
|
||||
|
||||
318
lib/data/services/chat_websocket_service.dart
Normal file
318
lib/data/services/chat_websocket_service.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../domain/entities/chat_message.dart';
|
||||
import '../models/chat_message_model.dart';
|
||||
|
||||
/// Service WebSocket pour la communication temps réel du chat.
|
||||
///
|
||||
/// Ce service gère :
|
||||
/// - La connexion WebSocket au serveur
|
||||
/// - La réception de nouveaux messages en temps réel
|
||||
/// - Les indicateurs de frappe (typing indicators)
|
||||
/// - Les confirmations de lecture (read receipts)
|
||||
class ChatWebSocketService extends ChangeNotifier {
|
||||
ChatWebSocketService(this.userId);
|
||||
|
||||
final String userId;
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
bool _isConnected = false;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
bool _isDisposed = false;
|
||||
Timer? _reconnectTimer;
|
||||
|
||||
// Streams pour les événements temps réel
|
||||
final _messageController = StreamController<ChatMessage>.broadcast();
|
||||
final _typingController = StreamController<TypingIndicator>.broadcast();
|
||||
final _readReceiptController = StreamController<ReadReceipt>.broadcast();
|
||||
final _deliveryController = StreamController<DeliveryConfirmation>.broadcast();
|
||||
|
||||
Stream<ChatMessage> get messageStream => _messageController.stream;
|
||||
Stream<TypingIndicator> get typingStream => _typingController.stream;
|
||||
Stream<ReadReceipt> get readReceiptStream => _readReceiptController.stream;
|
||||
Stream<DeliveryConfirmation> get deliveryStream => _deliveryController.stream;
|
||||
|
||||
/// Récupère l'URL WebSocket à partir de l'URL HTTP de base.
|
||||
String get _wsUrl {
|
||||
final baseUrl = EnvConfig.apiBaseUrl;
|
||||
// Remplacer http:// par ws:// ou https:// par wss://
|
||||
final wsUrl = baseUrl.replaceFirst('http://', 'ws://').replaceFirst('https://', 'wss://');
|
||||
return '$wsUrl/chat/ws/$userId';
|
||||
}
|
||||
|
||||
/// Se connecte au serveur WebSocket.
|
||||
Future<void> connect() async {
|
||||
if (_isDisposed) {
|
||||
AppLogger.w('Tentative de connexion après dispose, ignorée', tag: 'ChatWebSocketService');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isConnected) {
|
||||
AppLogger.w('Déjà connecté', tag: 'ChatWebSocketService');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AppLogger.i('Connexion à: $_wsUrl', tag: 'ChatWebSocketService');
|
||||
|
||||
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl));
|
||||
|
||||
// Écouter les messages entrants
|
||||
_subscription = _channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onError: _handleError,
|
||||
onDone: _handleDisconnection,
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
AppLogger.i('Connecté avec succès', tag: 'ChatWebSocketService');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion', error: e, stackTrace: stackTrace, tag: 'ChatWebSocketService');
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Déconnecte du serveur WebSocket.
|
||||
Future<void> disconnect() async {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
|
||||
if (!_isConnected) return;
|
||||
|
||||
AppLogger.i('Déconnexion...', tag: 'ChatWebSocketService');
|
||||
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
try {
|
||||
await _channel?.sink.close();
|
||||
} catch (e) {
|
||||
AppLogger.w('Erreur lors de la fermeture du canal: $e', tag: 'ChatWebSocketService');
|
||||
}
|
||||
_channel = null;
|
||||
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
AppLogger.i('Déconnecté', tag: 'ChatWebSocketService');
|
||||
}
|
||||
|
||||
/// Envoie un message via WebSocket.
|
||||
void sendMessage(ChatMessage message) {
|
||||
if (!_isConnected) {
|
||||
AppLogger.w('Erreur: Non connecté', tag: 'ChatWebSocketService');
|
||||
return;
|
||||
}
|
||||
|
||||
final model = ChatMessageModel.fromEntity(message);
|
||||
final payload = {
|
||||
'type': 'message',
|
||||
'data': model.toJson(),
|
||||
};
|
||||
|
||||
_channel?.sink.add(json.encode(payload));
|
||||
|
||||
AppLogger.d('Message envoyé: ${message.id}', tag: 'ChatWebSocketService');
|
||||
}
|
||||
|
||||
/// Envoie un indicateur de frappe.
|
||||
void sendTypingIndicator(String conversationId, bool isTyping) {
|
||||
if (!_isConnected) return;
|
||||
|
||||
final payload = {
|
||||
'type': 'typing',
|
||||
'data': {
|
||||
'conversationId': conversationId,
|
||||
'userId': userId,
|
||||
'isTyping': isTyping,
|
||||
},
|
||||
};
|
||||
|
||||
_channel?.sink.add(json.encode(payload));
|
||||
|
||||
AppLogger.d('Indicateur de frappe envoyé: $isTyping', tag: 'ChatWebSocketService');
|
||||
}
|
||||
|
||||
/// Envoie une confirmation de lecture.
|
||||
void sendReadReceipt(String messageId) {
|
||||
if (!_isConnected) return;
|
||||
|
||||
final payload = {
|
||||
'type': 'read',
|
||||
'data': {
|
||||
'messageId': messageId,
|
||||
'userId': userId,
|
||||
},
|
||||
};
|
||||
|
||||
_channel?.sink.add(json.encode(payload));
|
||||
|
||||
AppLogger.d('Confirmation de lecture envoyée: $messageId', tag: 'ChatWebSocketService');
|
||||
}
|
||||
|
||||
/// Gère les messages entrants du WebSocket.
|
||||
void _handleMessage(dynamic data) {
|
||||
try {
|
||||
final jsonData = json.decode(data as String) as Map<String, dynamic>;
|
||||
final type = jsonData['type'] as String?;
|
||||
final payload = jsonData['data'] as Map<String, dynamic>?;
|
||||
|
||||
if (payload == null) return;
|
||||
|
||||
switch (type) {
|
||||
case 'message':
|
||||
final message = ChatMessageModel.fromJson(payload).toEntity();
|
||||
_messageController.add(message);
|
||||
AppLogger.d('Nouveau message reçu: ${message.id}', tag: 'ChatWebSocketService');
|
||||
break;
|
||||
|
||||
case 'typing':
|
||||
final indicator = TypingIndicator(
|
||||
conversationId: payload['conversationId'] as String,
|
||||
userId: payload['userId'] as String,
|
||||
isTyping: payload['isTyping'] as bool,
|
||||
);
|
||||
_typingController.add(indicator);
|
||||
AppLogger.d('Indicateur de frappe: ${indicator.isTyping}', tag: 'ChatWebSocketService');
|
||||
break;
|
||||
|
||||
case 'read':
|
||||
AppLogger.d('RECEIVED READ: $payload', tag: 'ChatWebSocketService');
|
||||
final receipt = ReadReceipt(
|
||||
messageId: payload['messageId'] as String,
|
||||
userId: payload['userId'] as String,
|
||||
timestamp: DateTime.parse(payload['timestamp'] as String),
|
||||
);
|
||||
_readReceiptController.add(receipt);
|
||||
AppLogger.d('Confirmation lecture ajoutée au stream: ${receipt.messageId}', tag: 'ChatWebSocketService');
|
||||
break;
|
||||
|
||||
case 'delivered':
|
||||
AppLogger.d('RECEIVED DELIVERED: $payload', tag: 'ChatWebSocketService');
|
||||
final confirmation = DeliveryConfirmation(
|
||||
messageId: payload['messageId'] as String,
|
||||
isDelivered: payload['isDelivered'] as bool,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
payload['timestamp'] as int,
|
||||
),
|
||||
);
|
||||
_deliveryController.add(confirmation);
|
||||
AppLogger.d('Confirmation délivrance ajoutée au stream: ${confirmation.messageId}', tag: 'ChatWebSocketService');
|
||||
break;
|
||||
|
||||
default:
|
||||
AppLogger.w('Type de message inconnu: $type', tag: 'ChatWebSocketService');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur parsing message', error: e, stackTrace: stackTrace, tag: 'ChatWebSocketService');
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les erreurs WebSocket.
|
||||
void _handleError(Object error) {
|
||||
AppLogger.w('Erreur WebSocket: $error', tag: 'ChatWebSocketService');
|
||||
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Ne pas tenter de reconnexion si le backend ne supporte pas WebSocket
|
||||
// Le backend retourne "Connection was not upgraded to websocket"
|
||||
if (error.toString().contains('not upgraded to websocket')) {
|
||||
AppLogger.w('WebSocket non supporté par le backend, reconnexion désactivée', tag: 'ChatWebSocketService');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tentative de reconnexion après 5 secondes seulement si pas disposé
|
||||
if (!_isDisposed && _reconnectTimer == null) {
|
||||
_reconnectTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (!_isDisposed && !_isConnected) {
|
||||
AppLogger.i('Tentative de reconnexion...', tag: 'ChatWebSocketService');
|
||||
_reconnectTimer = null;
|
||||
connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère la déconnexion WebSocket.
|
||||
void _handleDisconnection() {
|
||||
AppLogger.i('Déconnexion détectée', tag: 'ChatWebSocketService');
|
||||
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
disconnect();
|
||||
_messageController.close();
|
||||
_typingController.close();
|
||||
_readReceiptController.close();
|
||||
_deliveryController.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicateur de frappe.
|
||||
class TypingIndicator {
|
||||
const TypingIndicator({
|
||||
required this.conversationId,
|
||||
required this.userId,
|
||||
required this.isTyping,
|
||||
});
|
||||
|
||||
final String conversationId;
|
||||
final String userId;
|
||||
final bool isTyping;
|
||||
}
|
||||
|
||||
/// Confirmation de lecture.
|
||||
class ReadReceipt {
|
||||
const ReadReceipt({
|
||||
required this.messageId,
|
||||
required this.userId,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
final String messageId;
|
||||
final String userId;
|
||||
final DateTime timestamp;
|
||||
}
|
||||
|
||||
/// Confirmation de délivrance.
|
||||
class DeliveryConfirmation {
|
||||
const DeliveryConfirmation({
|
||||
required this.messageId,
|
||||
required this.isDelivered,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
final String messageId;
|
||||
final bool isDelivered;
|
||||
final DateTime timestamp;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:flutter_bcrypt/flutter_bcrypt.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:afterwork/core/constants/urls.dart';
|
||||
import '../../core/constants/urls.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
class HashPasswordService {
|
||||
/// Hache le mot de passe en utilisant Bcrypt.
|
||||
/// Renvoie une chaîne hachée sécurisée.
|
||||
Future<String> hashPassword(String email, String password) async {
|
||||
try {
|
||||
print("Tentative de récupération du sel depuis le serveur pour l'email : $email");
|
||||
AppLogger.d("Tentative de récupération du sel depuis le serveur pour l'email : $email", tag: 'HashPasswordService');
|
||||
|
||||
// Récupérer le sel depuis le serveur avec l'email
|
||||
final response = await http.get(Uri.parse('${Urls.baseUrl}/users/salt?email=$email'));
|
||||
@@ -15,32 +16,32 @@ class HashPasswordService {
|
||||
String salt;
|
||||
if (response.statusCode == 200 && response.body.isNotEmpty) {
|
||||
salt = response.body;
|
||||
print("Sel récupéré depuis le serveur : $salt");
|
||||
AppLogger.d('Sel récupéré depuis le serveur : $salt', tag: 'HashPasswordService');
|
||||
} else {
|
||||
// Si le sel n'est pas trouvé, on en génère un
|
||||
salt = await FlutterBcrypt.saltWithRounds(rounds: 12);
|
||||
print("Sel généré : $salt");
|
||||
AppLogger.d('Sel généré : $salt', tag: 'HashPasswordService');
|
||||
}
|
||||
|
||||
// Hachage du mot de passe avec le sel
|
||||
String hashedPassword = await FlutterBcrypt.hashPw(password: password, salt: salt);
|
||||
print("Mot de passe haché avec succès : $hashedPassword");
|
||||
final String hashedPassword = await FlutterBcrypt.hashPw(password: password, salt: salt);
|
||||
AppLogger.d('Mot de passe haché avec succès', tag: 'HashPasswordService');
|
||||
return hashedPassword;
|
||||
} catch (e) {
|
||||
print("Erreur lors du hachage du mot de passe : $e");
|
||||
throw Exception("Erreur lors du hachage du mot de passe.");
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors du hachage du mot de passe', error: e, stackTrace: stackTrace, tag: 'HashPasswordService');
|
||||
throw Exception('Erreur lors du hachage du mot de passe.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> verifyPassword(String password, String hashedPassword) async {
|
||||
try {
|
||||
print("Début de la vérification du mot de passe");
|
||||
bool result = await FlutterBcrypt.verify(password: password, hash: hashedPassword);
|
||||
print("Résultat de la vérification : $result");
|
||||
AppLogger.d('Début de la vérification du mot de passe', tag: 'HashPasswordService');
|
||||
final bool result = await FlutterBcrypt.verify(password: password, hash: hashedPassword);
|
||||
AppLogger.d('Résultat de la vérification : $result', tag: 'HashPasswordService');
|
||||
return result;
|
||||
} catch (e) {
|
||||
print("Erreur lors de la vérification du mot de passe : $e");
|
||||
throw Exception("Erreur lors de la vérification du mot de passe.");
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur lors de la vérification du mot de passe', error: e, stackTrace: stackTrace, tag: 'HashPasswordService');
|
||||
throw Exception('Erreur lors de la vérification du mot de passe.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
173
lib/data/services/image_compression_service.dart
Normal file
173
lib/data/services/image_compression_service.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
|
||||
/// Configuration de compression d'image.
|
||||
class CompressionConfig {
|
||||
const CompressionConfig({
|
||||
this.quality = 85,
|
||||
this.maxWidth = 1920,
|
||||
this.maxHeight = 1920,
|
||||
this.format = CompressFormat.jpeg,
|
||||
});
|
||||
|
||||
final int quality; // 0-100
|
||||
final int maxWidth;
|
||||
final int maxHeight;
|
||||
final CompressFormat format;
|
||||
|
||||
/// Configuration pour les posts (équilibre qualité/taille)
|
||||
static const CompressionConfig post = CompressionConfig(
|
||||
quality: 85,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1920,
|
||||
);
|
||||
|
||||
/// Configuration pour les thumbnails (petite taille)
|
||||
static const CompressionConfig thumbnail = CompressionConfig(
|
||||
quality: 70,
|
||||
maxWidth: 400,
|
||||
maxHeight: 400,
|
||||
);
|
||||
|
||||
/// Configuration pour les stories (vertical, haute qualité)
|
||||
static const CompressionConfig story = CompressionConfig(
|
||||
quality: 90,
|
||||
maxWidth: 1080,
|
||||
maxHeight: 1920,
|
||||
);
|
||||
|
||||
/// Configuration pour les avatars (petit, carré)
|
||||
static const CompressionConfig avatar = CompressionConfig(
|
||||
quality: 80,
|
||||
maxWidth: 500,
|
||||
maxHeight: 500,
|
||||
);
|
||||
}
|
||||
|
||||
/// Service de compression d'images.
|
||||
///
|
||||
/// Compresse les images avant l'upload pour réduire la bande passante
|
||||
/// et améliorer les performances.
|
||||
class ImageCompressionService {
|
||||
/// Compresse une image selon la configuration donnée.
|
||||
Future<XFile?> compressImage(
|
||||
XFile file, {
|
||||
CompressionConfig config = CompressionConfig.post,
|
||||
}) async {
|
||||
try {
|
||||
final filePath = file.path;
|
||||
final lastIndex = filePath.lastIndexOf('.');
|
||||
final splitted = filePath.substring(0, lastIndex);
|
||||
final outPath = '${splitted}_compressed${path.extension(filePath)}';
|
||||
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
final originalSize = await File(filePath).length();
|
||||
debugPrint('[ImageCompression] Compression de: ${path.basename(filePath)}');
|
||||
debugPrint('[ImageCompression] Taille originale: ${_formatBytes(originalSize)}');
|
||||
}
|
||||
|
||||
final result = await FlutterImageCompress.compressAndGetFile(
|
||||
filePath,
|
||||
outPath,
|
||||
quality: config.quality,
|
||||
minWidth: config.maxWidth,
|
||||
minHeight: config.maxHeight,
|
||||
format: config.format,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
final compressedSize = await File(result.path).length();
|
||||
final originalSize = await File(filePath).length();
|
||||
final reduction = ((1 - compressedSize / originalSize) * 100).toStringAsFixed(1);
|
||||
debugPrint('[ImageCompression] Taille compressée: ${_formatBytes(compressedSize)}');
|
||||
debugPrint('[ImageCompression] Réduction: $reduction%');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('[ImageCompression] Erreur: $e');
|
||||
// En cas d'erreur, on retourne le fichier original
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compresse plusieurs images en parallèle.
|
||||
Future<List<XFile>> compressMultipleImages(
|
||||
List<XFile> files, {
|
||||
CompressionConfig config = CompressionConfig.post,
|
||||
void Function(int processed, int total)? onProgress,
|
||||
}) async {
|
||||
final results = <XFile>[];
|
||||
int processed = 0;
|
||||
|
||||
for (final file in files) {
|
||||
// Ne compresser que les images, pas les vidéos
|
||||
if (_isImageFile(file.path)) {
|
||||
final compressed = await compressImage(file, config: config);
|
||||
results.add(compressed ?? file);
|
||||
} else {
|
||||
results.add(file);
|
||||
}
|
||||
|
||||
processed++;
|
||||
if (onProgress != null) {
|
||||
onProgress(processed, files.length);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Crée un thumbnail à partir d'une image.
|
||||
Future<XFile?> createThumbnail(XFile file) async {
|
||||
return compressImage(file, config: CompressionConfig.thumbnail);
|
||||
}
|
||||
|
||||
/// Vérifie si le fichier est une image.
|
||||
bool _isImageFile(String filePath) {
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
||||
final extension = path.extension(filePath).toLowerCase();
|
||||
return imageExtensions.contains(extension);
|
||||
}
|
||||
|
||||
/// Formate la taille en bytes de manière lisible.
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
|
||||
/// Nettoie les fichiers temporaires compressés.
|
||||
Future<void> cleanupTempFiles() async {
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final files = tempDir.listSync();
|
||||
|
||||
for (final file in files) {
|
||||
if (file.path.contains('_compressed')) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[ImageCompression] Fichiers temporaires nettoyés');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ImageCompression] Erreur nettoyage: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
199
lib/data/services/media_upload_service.dart
Normal file
199
lib/data/services/media_upload_service.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:video_thumbnail/video_thumbnail.dart' as video_thumb;
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
|
||||
/// Résultat d'un upload de média.
|
||||
class MediaUploadResult {
|
||||
const MediaUploadResult({
|
||||
required this.url,
|
||||
required this.thumbnailUrl,
|
||||
required this.type,
|
||||
this.duration,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final String? thumbnailUrl;
|
||||
final String type; // 'image' ou 'video'
|
||||
final Duration? duration;
|
||||
}
|
||||
|
||||
/// Service d'upload de médias vers le backend.
|
||||
///
|
||||
/// Gère l'upload d'images et de vidéos avec compression et génération de thumbnails.
|
||||
class MediaUploadService {
|
||||
MediaUploadService(this._client);
|
||||
|
||||
final http.Client _client;
|
||||
|
||||
/// URL de base pour l'upload (à configurer selon votre backend)
|
||||
static const String _uploadEndpoint = '${EnvConfig.apiBaseUrl}/media/upload';
|
||||
|
||||
/// Upload un seul média (image ou vidéo).
|
||||
Future<MediaUploadResult> uploadMedia(XFile file) async {
|
||||
try {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[MediaUploadService] Upload de: ${file.path}');
|
||||
}
|
||||
|
||||
final fileExtension = path.extension(file.path).toLowerCase();
|
||||
final isVideo = _isVideoFile(fileExtension);
|
||||
|
||||
// Créer la requête multipart
|
||||
final request = http.MultipartRequest('POST', Uri.parse(_uploadEndpoint));
|
||||
|
||||
// Ajouter le fichier
|
||||
final fileBytes = await file.readAsBytes();
|
||||
final multipartFile = http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
fileBytes,
|
||||
filename: path.basename(file.path),
|
||||
);
|
||||
request.files.add(multipartFile);
|
||||
|
||||
// Ajouter le type
|
||||
request.fields['type'] = isVideo ? 'video' : 'image';
|
||||
|
||||
// Envoyer la requête
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// Parser la réponse JSON du backend
|
||||
final responseData = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
// Format attendu du backend:
|
||||
// {
|
||||
// "url": "https://...",
|
||||
// "thumbnailUrl": "https://...", (optionnel)
|
||||
// "type": "image" ou "video",
|
||||
// "duration": 60 (en secondes, optionnel)
|
||||
// }
|
||||
|
||||
final url = responseData['url'] as String? ??
|
||||
'https://example.com/media/${path.basename(file.path)}';
|
||||
final thumbnailUrl = responseData['thumbnailUrl'] as String?;
|
||||
final typeFromBackend = responseData['type'] as String?;
|
||||
final durationSeconds = responseData['duration'] as int?;
|
||||
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[MediaUploadService] Upload réussi: $url');
|
||||
}
|
||||
|
||||
return MediaUploadResult(
|
||||
url: url,
|
||||
thumbnailUrl: thumbnailUrl,
|
||||
type: typeFromBackend ?? (isVideo ? 'video' : 'image'),
|
||||
duration: durationSeconds != null
|
||||
? Duration(seconds: durationSeconds)
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
throw Exception(
|
||||
'Échec de l\'upload: ${response.statusCode} - ${response.body}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MediaUploadService] Erreur: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload plusieurs médias en parallèle.
|
||||
Future<List<MediaUploadResult>> uploadMultipleMedias(
|
||||
List<XFile> files, {
|
||||
void Function(int uploaded, int total)? onProgress,
|
||||
}) async {
|
||||
final results = <MediaUploadResult>[];
|
||||
int uploaded = 0;
|
||||
|
||||
for (final file in files) {
|
||||
try {
|
||||
final result = await uploadMedia(file);
|
||||
results.add(result);
|
||||
uploaded++;
|
||||
|
||||
if (onProgress != null) {
|
||||
onProgress(uploaded, files.length);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MediaUploadService] Échec upload ${file.path}: $e');
|
||||
// On continue avec les autres fichiers
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Vérifie si le fichier est une vidéo.
|
||||
bool _isVideoFile(String extension) {
|
||||
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.m4v'];
|
||||
return videoExtensions.contains(extension);
|
||||
}
|
||||
|
||||
/// Supprime un média du serveur.
|
||||
Future<void> deleteMedia(String mediaUrl) async {
|
||||
try {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[MediaUploadService] Suppression de: $mediaUrl');
|
||||
}
|
||||
|
||||
// Extraire l'ID ou le nom du fichier de l'URL
|
||||
final uri = Uri.parse(mediaUrl);
|
||||
final fileName = uri.pathSegments.last;
|
||||
|
||||
// Appel API pour supprimer le média
|
||||
final deleteUrl = '${EnvConfig.apiBaseUrl}/media/$fileName';
|
||||
final response = await _client.delete(
|
||||
Uri.parse(deleteUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[MediaUploadService] Média supprimé: $mediaUrl');
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
'Échec de la suppression: ${response.statusCode} - ${response.body}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MediaUploadService] Erreur suppression: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère un thumbnail pour une vidéo.
|
||||
Future<String?> generateVideoThumbnail(String videoPath) async {
|
||||
try {
|
||||
if (EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[MediaUploadService] Génération thumbnail pour: $videoPath');
|
||||
}
|
||||
|
||||
// Générer le thumbnail à partir de la vidéo
|
||||
final thumbnailPath = await video_thumb.VideoThumbnail.thumbnailFile(
|
||||
video: videoPath,
|
||||
thumbnailPath: (await Directory.systemTemp.createTemp()).path,
|
||||
imageFormat: video_thumb.ImageFormat.JPEG,
|
||||
maxWidth: 640,
|
||||
quality: 75,
|
||||
);
|
||||
|
||||
if (thumbnailPath != null && EnvConfig.enableDetailedLogs) {
|
||||
debugPrint('[MediaUploadService] Thumbnail généré: $thumbnailPath');
|
||||
}
|
||||
|
||||
return thumbnailPath;
|
||||
} catch (e) {
|
||||
debugPrint('[MediaUploadService] Erreur génération thumbnail: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
246
lib/data/services/notification_service.dart
Normal file
246
lib/data/services/notification_service.dart
Normal file
@@ -0,0 +1,246 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../domain/entities/notification.dart' as domain;
|
||||
import '../datasources/notification_remote_data_source.dart';
|
||||
import '../services/realtime_notification_service.dart';
|
||||
import '../services/secure_storage.dart';
|
||||
|
||||
/// Service centralisé de gestion des notifications.
|
||||
///
|
||||
/// Ce service gère:
|
||||
/// - Le compteur de notifications non lues
|
||||
/// - Le chargement des notifications depuis l'API
|
||||
/// - Les notifications in-app
|
||||
/// - Les listeners pour les changements
|
||||
///
|
||||
/// **Usage avec Provider:**
|
||||
/// ```dart
|
||||
/// // Dans main.dart
|
||||
/// ChangeNotifierProvider(
|
||||
/// create: (_) => NotificationService(
|
||||
/// NotificationRemoteDataSource(http.Client()),
|
||||
/// SecureStorage(),
|
||||
/// )..initialize(),
|
||||
/// ),
|
||||
///
|
||||
/// // Dans un widget
|
||||
/// final notificationService = Provider.of<NotificationService>(context);
|
||||
/// final unreadCount = notificationService.unreadCount;
|
||||
/// ```
|
||||
class NotificationService extends ChangeNotifier {
|
||||
NotificationService(this._dataSource, this._secureStorage);
|
||||
|
||||
final NotificationRemoteDataSource _dataSource;
|
||||
final SecureStorage _secureStorage;
|
||||
|
||||
List<domain.Notification> _notifications = [];
|
||||
bool _isLoading = false;
|
||||
Timer? _refreshTimer;
|
||||
|
||||
// Service de notifications temps réel
|
||||
RealtimeNotificationService? _realtimeService;
|
||||
StreamSubscription<SystemNotification>? _systemNotificationSubscription;
|
||||
|
||||
/// Liste de toutes les notifications
|
||||
List<domain.Notification> get notifications => List.unmodifiable(_notifications);
|
||||
|
||||
/// Nombre total de notifications
|
||||
int get totalCount => _notifications.length;
|
||||
|
||||
/// Nombre de notifications non lues
|
||||
int get unreadCount => _notifications.where((n) => !n.isRead).length;
|
||||
|
||||
/// Indique si les notifications sont en cours de chargement
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
/// Initialise le service (à appeler au démarrage de l'app).
|
||||
Future<void> initialize() async {
|
||||
await loadNotifications();
|
||||
// Actualise les notifications toutes les 2 minutes
|
||||
_startPeriodicRefresh();
|
||||
}
|
||||
|
||||
/// Charge les notifications depuis l'API.
|
||||
Future<void> loadNotifications() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final userId = await _secureStorage.getUserId();
|
||||
if (userId == null || userId.isEmpty) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final notificationModels = await _dataSource.getNotifications(userId);
|
||||
_notifications = notificationModels.map((model) => model.toEntity()).toList();
|
||||
|
||||
// Trie par timestamp décroissant (les plus récentes en premier)
|
||||
_notifications.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
} catch (e) {
|
||||
debugPrint('[NotificationService] Erreur chargement: $e');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque une notification comme lue.
|
||||
Future<void> markAsRead(String notificationId) async {
|
||||
try {
|
||||
await _dataSource.markAsRead(notificationId);
|
||||
|
||||
// Mise à jour locale
|
||||
final index = _notifications.indexWhere((n) => n.id == notificationId);
|
||||
if (index != -1) {
|
||||
_notifications[index] = _notifications[index].copyWith(isRead: true);
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[NotificationService] Erreur marquage lu: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque toutes les notifications comme lues.
|
||||
Future<void> markAllAsRead() async {
|
||||
try {
|
||||
final userId = await _secureStorage.getUserId();
|
||||
if (userId == null) return;
|
||||
|
||||
await _dataSource.markAllAsRead(userId);
|
||||
|
||||
// Mise à jour locale
|
||||
_notifications = _notifications.map((n) => n.copyWith(isRead: true)).toList();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('[NotificationService] Erreur marquage tout lu: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une notification.
|
||||
Future<void> deleteNotification(String notificationId) async {
|
||||
try {
|
||||
await _dataSource.deleteNotification(notificationId);
|
||||
|
||||
// Mise à jour locale
|
||||
_notifications.removeWhere((n) => n.id == notificationId);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('[NotificationService] Erreur suppression: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute une nouvelle notification (simulation ou depuis push notification).
|
||||
///
|
||||
/// Utilisé pour afficher une notification in-app quand une nouvelle
|
||||
/// notification arrive via Firebase Cloud Messaging.
|
||||
void addNotification(domain.Notification notification) {
|
||||
_notifications.insert(0, notification);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Démarre l'actualisation périodique des notifications.
|
||||
void _startPeriodicRefresh() {
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = Timer.periodic(
|
||||
const Duration(minutes: 2),
|
||||
(_) => loadNotifications(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Arrête l'actualisation périodique.
|
||||
void stopPeriodicRefresh() {
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = null;
|
||||
}
|
||||
|
||||
/// Connecte le service de notifications temps réel.
|
||||
///
|
||||
/// Cette méthode remplace le polling par des notifications push en temps réel.
|
||||
/// Le polling est automatiquement désactivé lorsque le service temps réel est connecté.
|
||||
///
|
||||
/// [service] : Le service de notifications temps réel à connecter.
|
||||
void connectRealtime(RealtimeNotificationService service) {
|
||||
_realtimeService = service;
|
||||
|
||||
// IMPORTANT : Arrêter le polling puisqu'on passe en temps réel
|
||||
stopPeriodicRefresh();
|
||||
debugPrint('[NotificationService] Polling arrêté, passage en mode temps réel');
|
||||
|
||||
// Écouter les notifications système en temps réel
|
||||
_systemNotificationSubscription = service.systemNotificationStream.listen(
|
||||
_handleSystemNotification,
|
||||
onError: (error) {
|
||||
debugPrint('[NotificationService] Erreur dans le stream de notifications système: $error');
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('[NotificationService] Service de notifications temps réel connecté');
|
||||
}
|
||||
|
||||
/// Gère les notifications système reçues en temps réel.
|
||||
///
|
||||
/// Cette méthode est appelée automatiquement lorsqu'une notification
|
||||
/// est reçue via WebSocket.
|
||||
void _handleSystemNotification(SystemNotification notification) {
|
||||
debugPrint('[NotificationService] Notification système reçue: ${notification.title}');
|
||||
|
||||
// Convertir en entité domain
|
||||
final domainNotification = domain.Notification(
|
||||
id: notification.notificationId,
|
||||
title: notification.title,
|
||||
message: notification.message,
|
||||
type: _parseNotificationType(notification.type),
|
||||
timestamp: notification.timestamp,
|
||||
isRead: false,
|
||||
eventId: null,
|
||||
userId: '', // Le userId sera récupéré du contexte
|
||||
metadata: null,
|
||||
);
|
||||
|
||||
// Ajouter à la liste locale (en tête de liste pour avoir les plus récentes en premier)
|
||||
addNotification(domainNotification);
|
||||
|
||||
debugPrint('[NotificationService] Notification ajoutée à la liste locale: ${notification.title}');
|
||||
}
|
||||
|
||||
/// Parse le type de notification depuis une chaîne.
|
||||
domain.NotificationType _parseNotificationType(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'event':
|
||||
return domain.NotificationType.event;
|
||||
case 'friend':
|
||||
return domain.NotificationType.friend;
|
||||
case 'reminder':
|
||||
return domain.NotificationType.reminder;
|
||||
default:
|
||||
return domain.NotificationType.other;
|
||||
}
|
||||
}
|
||||
|
||||
/// Déconnecte le service de notifications temps réel.
|
||||
///
|
||||
/// Le polling est automatiquement redémarré lorsque le service est déconnecté.
|
||||
void disconnectRealtime() {
|
||||
_systemNotificationSubscription?.cancel();
|
||||
_systemNotificationSubscription = null;
|
||||
_realtimeService = null;
|
||||
|
||||
// Redémarrer le polling si déconnecté du temps réel
|
||||
_startPeriodicRefresh();
|
||||
debugPrint('[NotificationService] Service temps réel déconnecté, reprise du polling');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnectRealtime();
|
||||
stopPeriodicRefresh();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// Classe pour gérer les préférences utilisateur à l'aide de SharedPreferences.
|
||||
/// Permet de stocker et récupérer des informations de manière non sécurisée,
|
||||
/// contrairement au stockage sécurisé qui est utilisé pour des données sensibles.
|
||||
@@ -11,88 +13,88 @@ class PreferencesHelper {
|
||||
/// Sauvegarde une chaîne de caractères (String) dans les préférences.
|
||||
/// Les actions sont loguées et les erreurs capturées pour garantir une sauvegarde correcte.
|
||||
Future<void> setString(String key, String value) async {
|
||||
print("[LOG] Sauvegarde dans les préférences : clé = $key, valeur = $value");
|
||||
AppLogger.d('Sauvegarde dans les préférences : clé = $key, valeur = $value', tag: 'PreferencesHelper');
|
||||
final prefs = await _prefs;
|
||||
final success = await prefs.setString(key, value);
|
||||
if (success) {
|
||||
print("[LOG] Sauvegarde réussie pour la clé : $key");
|
||||
AppLogger.d('Sauvegarde réussie pour la clé : $key', tag: 'PreferencesHelper');
|
||||
} else {
|
||||
print("[ERROR] Échec de la sauvegarde pour la clé : $key");
|
||||
AppLogger.e('Échec de la sauvegarde pour la clé : $key', tag: 'PreferencesHelper');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère une chaîne de caractères depuis les préférences.
|
||||
/// Retourne la valeur ou null si aucune donnée n'est trouvée.
|
||||
Future<String?> getString(String key) async {
|
||||
print("[LOG] Récupération depuis les préférences pour la clé : $key");
|
||||
AppLogger.d('Récupération depuis les préférences pour la clé : $key', tag: 'PreferencesHelper');
|
||||
final prefs = await _prefs;
|
||||
final value = prefs.getString(key);
|
||||
print("[LOG] Valeur récupérée pour la clé $key : $value");
|
||||
AppLogger.d('Valeur récupérée pour la clé $key : $value', tag: 'PreferencesHelper');
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Supprime une entrée dans les préférences.
|
||||
/// Logue chaque étape de la suppression.
|
||||
Future<void> remove(String key) async {
|
||||
print("[LOG] Suppression dans les préférences pour la clé : $key");
|
||||
AppLogger.d('Suppression dans les préférences pour la clé : $key', tag: 'PreferencesHelper');
|
||||
final prefs = await _prefs;
|
||||
final success = await prefs.remove(key);
|
||||
if (success) {
|
||||
print("[LOG] Suppression réussie pour la clé : $key");
|
||||
AppLogger.d('Suppression réussie pour la clé : $key', tag: 'PreferencesHelper');
|
||||
} else {
|
||||
print("[ERROR] Échec de la suppression pour la clé : $key");
|
||||
AppLogger.e('Échec de la suppression pour la clé : $key', tag: 'PreferencesHelper');
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde l'identifiant utilisateur dans les préférences.
|
||||
/// Logue l'action et assure la robustesse de l'opération.
|
||||
Future<void> saveUserId(String userId) async {
|
||||
print("[LOG] Sauvegarde de l'userId dans les préférences : $userId");
|
||||
AppLogger.d("Sauvegarde de l'userId dans les préférences : $userId", tag: 'PreferencesHelper');
|
||||
await setString('user_id', userId);
|
||||
}
|
||||
|
||||
/// Récupère l'identifiant utilisateur depuis les préférences.
|
||||
/// Retourne l'ID ou null en cas d'échec.
|
||||
Future<String?> getUserId() async {
|
||||
print("[LOG] Récupération de l'userId depuis les préférences.");
|
||||
return await getString('user_id');
|
||||
AppLogger.d("Récupération de l'userId depuis les préférences.", tag: 'PreferencesHelper');
|
||||
return getString('user_id');
|
||||
}
|
||||
|
||||
/// Sauvegarde le nom d'utilisateur dans les préférences.
|
||||
/// Logue l'opération pour assurer un suivi complet.
|
||||
Future<void> saveUserName(String userFirstName) async {
|
||||
print("[LOG] Sauvegarde du userFirstName dans les préférences : $userFirstName");
|
||||
AppLogger.d('Sauvegarde du userFirstName dans les préférences : $userFirstName', tag: 'PreferencesHelper');
|
||||
await setString('user_name', userFirstName);
|
||||
}
|
||||
|
||||
/// Récupère le nom d'utilisateur depuis les préférences.
|
||||
/// Retourne le nom ou null en cas d'échec.
|
||||
Future<String?> getUseFirstrName() async {
|
||||
print("[LOG] Récupération du userFirstName depuis les préférences.");
|
||||
return await getString('user_name');
|
||||
AppLogger.d('Récupération du userFirstName depuis les préférences.', tag: 'PreferencesHelper');
|
||||
return getString('user_name');
|
||||
}
|
||||
|
||||
/// Sauvegarde le prénom de l'utilisateur dans les préférences.
|
||||
/// Logue l'opération pour assurer un suivi complet.
|
||||
Future<void> saveUserLastName(String userLastName) async {
|
||||
print("[LOG] Sauvegarde du userLastName dans les préférences : $userLastName");
|
||||
AppLogger.d('Sauvegarde du userLastName dans les préférences : $userLastName', tag: 'PreferencesHelper');
|
||||
await setString('user_last_name', userLastName);
|
||||
}
|
||||
|
||||
/// Récupère le prénom de l'utilisateur depuis les préférences.
|
||||
/// Retourne le prénom ou null en cas d'échec.
|
||||
Future<String?> getUserLastName() async {
|
||||
print("[LOG] Récupération du userLastName depuis les préférences.");
|
||||
return await getString('user_last_name');
|
||||
AppLogger.d('Récupération du userLastName depuis les préférences.', tag: 'PreferencesHelper');
|
||||
return getString('user_last_name');
|
||||
}
|
||||
|
||||
/// Supprime toutes les informations utilisateur dans les préférences.
|
||||
/// Logue chaque étape de la suppression.
|
||||
Future<void> clearUserInfo() async {
|
||||
print("[LOG] Suppression des informations utilisateur (userId, userFirstName, userLastName) des préférences.");
|
||||
AppLogger.d('Suppression des informations utilisateur (userId, userFirstName, userLastName) des préférences.', tag: 'PreferencesHelper');
|
||||
await remove('user_id');
|
||||
await remove('user_name');
|
||||
await remove('user_last_name');
|
||||
print("[LOG] Suppression réussie des informations utilisateur.");
|
||||
AppLogger.d('Suppression réussie des informations utilisateur.', tag: 'PreferencesHelper');
|
||||
}
|
||||
}
|
||||
|
||||
421
lib/data/services/realtime_notification_service.dart
Normal file
421
lib/data/services/realtime_notification_service.dart
Normal file
@@ -0,0 +1,421 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../../core/constants/env_config.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// Service WebSocket pour les notifications en temps réel.
|
||||
///
|
||||
/// Ce service gère les connexions WebSocket pour recevoir :
|
||||
/// - Demandes d'amitié (envoi, réception, acceptation, rejet)
|
||||
/// - Notifications système (événements, rappels)
|
||||
/// - Alertes de messages
|
||||
///
|
||||
/// **Architecture :**
|
||||
/// - Connexion WebSocket persistante à `/notifications/ws/{userId}`
|
||||
/// - Streams séparés par type de notification
|
||||
/// - Reconnexion automatique en cas de déconnexion
|
||||
/// - Support multi-sessions (l'utilisateur peut être connecté sur plusieurs appareils)
|
||||
class RealtimeNotificationService extends ChangeNotifier {
|
||||
RealtimeNotificationService(this.userId);
|
||||
|
||||
final String userId;
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
bool _isConnected = false;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
bool _isDisposed = false;
|
||||
Timer? _reconnectTimer;
|
||||
|
||||
// Streams pour différents types d'événements
|
||||
final _friendRequestController = StreamController<FriendRequestNotification>.broadcast();
|
||||
final _systemNotificationController = StreamController<SystemNotification>.broadcast();
|
||||
final _messageAlertController = StreamController<MessageAlert>.broadcast();
|
||||
final _presenceController = StreamController<PresenceUpdate>.broadcast();
|
||||
|
||||
Stream<FriendRequestNotification> get friendRequestStream => _friendRequestController.stream;
|
||||
Stream<SystemNotification> get systemNotificationStream => _systemNotificationController.stream;
|
||||
Stream<MessageAlert> get messageAlertStream => _messageAlertController.stream;
|
||||
Stream<PresenceUpdate> get presenceStream => _presenceController.stream;
|
||||
|
||||
/// Récupère l'URL WebSocket à partir de l'URL HTTP de base.
|
||||
String get _wsUrl {
|
||||
final baseUrl = EnvConfig.apiBaseUrl;
|
||||
// Remplacer http:// par ws:// ou https:// par wss://
|
||||
final wsUrl = baseUrl.replaceFirst('http://', 'ws://').replaceFirst('https://', 'wss://');
|
||||
return '$wsUrl/notifications/ws/$userId';
|
||||
}
|
||||
|
||||
/// Se connecte au serveur WebSocket.
|
||||
Future<void> connect() async {
|
||||
if (_isDisposed) {
|
||||
AppLogger.w('Tentative de connexion après dispose, ignorée', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isConnected) {
|
||||
AppLogger.w('Déjà connecté', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AppLogger.i('Connexion à: $_wsUrl', tag: 'RealtimeNotificationService');
|
||||
|
||||
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl));
|
||||
|
||||
// Écouter les messages entrants
|
||||
_subscription = _channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onError: _handleError,
|
||||
onDone: _handleDisconnection,
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
AppLogger.i('Connecté avec succès au service de notifications', tag: 'RealtimeNotificationService');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur de connexion', error: e, stackTrace: stackTrace, tag: 'RealtimeNotificationService');
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Déconnecte du serveur WebSocket.
|
||||
Future<void> disconnect() async {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
|
||||
if (!_isConnected) return;
|
||||
|
||||
AppLogger.i('Déconnexion...', tag: 'RealtimeNotificationService');
|
||||
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
try {
|
||||
await _channel?.sink.close();
|
||||
} catch (e) {
|
||||
AppLogger.w('Erreur lors de la fermeture du canal: $e', tag: 'RealtimeNotificationService');
|
||||
}
|
||||
_channel = null;
|
||||
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
AppLogger.i('Déconnecté du service de notifications', tag: 'RealtimeNotificationService');
|
||||
}
|
||||
|
||||
/// Envoie un ping pour maintenir la connexion active (keep-alive).
|
||||
void sendPing() {
|
||||
if (!_isConnected) {
|
||||
AppLogger.w('Impossible d\'envoyer un ping: non connecté', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
final payload = {
|
||||
'type': 'ping',
|
||||
};
|
||||
|
||||
_channel?.sink.add(json.encode(payload));
|
||||
AppLogger.d('Ping envoyé', tag: 'RealtimeNotificationService');
|
||||
}
|
||||
|
||||
/// Envoie un heartbeat pour maintenir le statut online.
|
||||
void sendHeartbeat() {
|
||||
sendPing(); // Le ping est géré côté serveur pour mettre à jour la présence
|
||||
}
|
||||
|
||||
/// Envoie un accusé de réception pour une notification.
|
||||
void sendAcknowledgement(String notificationId) {
|
||||
if (!_isConnected) return;
|
||||
|
||||
final payload = {
|
||||
'type': 'ack',
|
||||
'data': {
|
||||
'notificationId': notificationId,
|
||||
},
|
||||
};
|
||||
|
||||
_channel?.sink.add(json.encode(payload));
|
||||
AppLogger.d('ACK envoyé pour notification: $notificationId', tag: 'RealtimeNotificationService');
|
||||
}
|
||||
|
||||
/// Gère les messages entrants du WebSocket.
|
||||
void _handleMessage(dynamic data) {
|
||||
try {
|
||||
final jsonData = json.decode(data as String) as Map<String, dynamic>;
|
||||
final type = jsonData['type'] as String?;
|
||||
final payload = jsonData['data'] as Map<String, dynamic>?;
|
||||
|
||||
if (type == 'connected') {
|
||||
AppLogger.i('Confirmation de connexion reçue', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == 'pong') {
|
||||
AppLogger.d('Pong reçu', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload == null) {
|
||||
AppLogger.w('Payload null pour le type: $type', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'friend_request_received':
|
||||
final notification = FriendRequestNotification.fromJson(payload, type: 'received');
|
||||
_friendRequestController.add(notification);
|
||||
AppLogger.i('Demande d\'amitié reçue de: ${notification.senderName}', tag: 'RealtimeNotificationService');
|
||||
break;
|
||||
|
||||
case 'friend_request_accepted':
|
||||
final notification = FriendRequestNotification.fromJson(payload, type: 'accepted');
|
||||
_friendRequestController.add(notification);
|
||||
AppLogger.i('Demande d\'amitié acceptée par: ${notification.senderName}', tag: 'RealtimeNotificationService');
|
||||
break;
|
||||
|
||||
case 'friend_request_rejected':
|
||||
final notification = FriendRequestNotification.fromJson(payload, type: 'rejected');
|
||||
_friendRequestController.add(notification);
|
||||
AppLogger.i('Demande d\'amitié rejetée: ${notification.requestId}', tag: 'RealtimeNotificationService');
|
||||
break;
|
||||
|
||||
case 'message_received':
|
||||
final alert = MessageAlert.fromJson(payload);
|
||||
_messageAlertController.add(alert);
|
||||
AppLogger.i('Nouveau message de: ${alert.senderName}', tag: 'RealtimeNotificationService');
|
||||
break;
|
||||
|
||||
case 'system_notification':
|
||||
final notification = SystemNotification.fromJson(payload);
|
||||
_systemNotificationController.add(notification);
|
||||
AppLogger.i('Notification système: ${notification.title}', tag: 'RealtimeNotificationService');
|
||||
break;
|
||||
|
||||
case 'presence':
|
||||
final update = PresenceUpdate.fromJson(payload);
|
||||
_presenceController.add(update);
|
||||
AppLogger.i('Mise à jour présence: ${update.userId} -> ${update.isOnline}', tag: 'RealtimeNotificationService');
|
||||
break;
|
||||
|
||||
default:
|
||||
AppLogger.w('Type de notification inconnu: $type', tag: 'RealtimeNotificationService');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Erreur parsing notification', error: e, stackTrace: stackTrace, tag: 'RealtimeNotificationService');
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les erreurs WebSocket.
|
||||
void _handleError(Object error) {
|
||||
AppLogger.w('Erreur WebSocket: $error', tag: 'RealtimeNotificationService');
|
||||
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Ne pas tenter de reconnexion si le backend ne supporte pas WebSocket
|
||||
if (error.toString().contains('not upgraded to websocket')) {
|
||||
AppLogger.w('WebSocket non supporté par le backend, reconnexion désactivée', tag: 'RealtimeNotificationService');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tentative de reconnexion après 5 secondes seulement si pas disposé
|
||||
if (!_isDisposed && _reconnectTimer == null) {
|
||||
_reconnectTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (!_isDisposed && !_isConnected) {
|
||||
AppLogger.i('Tentative de reconnexion...', tag: 'RealtimeNotificationService');
|
||||
_reconnectTimer = null;
|
||||
connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère la déconnexion WebSocket.
|
||||
void _handleDisconnection() {
|
||||
AppLogger.i('Déconnexion détectée', tag: 'RealtimeNotificationService');
|
||||
|
||||
_isConnected = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Tenter une reconnexion
|
||||
if (!_isDisposed && _reconnectTimer == null) {
|
||||
_reconnectTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (!_isDisposed && !_isConnected) {
|
||||
AppLogger.i('Reconnexion après déconnexion...', tag: 'RealtimeNotificationService');
|
||||
_reconnectTimer = null;
|
||||
connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
disconnect();
|
||||
_friendRequestController.close();
|
||||
_systemNotificationController.close();
|
||||
_messageAlertController.close();
|
||||
_presenceController.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification de demande d'amitié.
|
||||
class FriendRequestNotification {
|
||||
const FriendRequestNotification({
|
||||
required this.type,
|
||||
required this.requestId,
|
||||
required this.senderId,
|
||||
required this.senderName,
|
||||
required this.senderProfileImage,
|
||||
});
|
||||
|
||||
/// Type: 'received', 'accepted', 'rejected'
|
||||
final String type;
|
||||
|
||||
/// ID de la demande d'amitié (friendshipId)
|
||||
final String requestId;
|
||||
|
||||
/// ID de l'utilisateur qui a envoyé/accepté/rejeté
|
||||
final String senderId;
|
||||
|
||||
/// Nom complet de l'utilisateur
|
||||
final String senderName;
|
||||
|
||||
/// URL de l'image de profil
|
||||
final String senderProfileImage;
|
||||
|
||||
factory FriendRequestNotification.fromJson(Map<String, dynamic> json, {required String type}) {
|
||||
return FriendRequestNotification(
|
||||
type: type,
|
||||
requestId: json['requestId']?.toString() ?? json['friendshipId']?.toString() ?? '',
|
||||
senderId: json['senderId']?.toString() ?? json['accepterId']?.toString() ?? '',
|
||||
senderName: json['senderName']?.toString() ?? json['acceptedBy']?.toString() ?? 'Utilisateur',
|
||||
senderProfileImage: json['senderProfileImage']?.toString() ?? json['accepterProfileImage']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
String toDisplayMessage() {
|
||||
switch (type) {
|
||||
case 'received':
|
||||
return 'Nouvelle demande d\'amitié de $senderName';
|
||||
case 'accepted':
|
||||
return '$senderName a accepté votre demande';
|
||||
case 'rejected':
|
||||
return 'Demande d\'amitié refusée';
|
||||
default:
|
||||
return 'Notification de demande d\'amitié';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification système.
|
||||
class SystemNotification {
|
||||
const SystemNotification({
|
||||
required this.notificationId,
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.type,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
final String notificationId;
|
||||
final String title;
|
||||
final String message;
|
||||
final String type; // 'event', 'friend', 'reminder', 'other'
|
||||
final DateTime timestamp;
|
||||
|
||||
factory SystemNotification.fromJson(Map<String, dynamic> json) {
|
||||
return SystemNotification(
|
||||
notificationId: json['notificationId']?.toString() ?? '',
|
||||
title: json['title']?.toString() ?? 'Notification',
|
||||
message: json['message']?.toString() ?? '',
|
||||
type: json['type']?.toString() ?? 'other',
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.parse(json['timestamp'].toString())
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Alerte de message.
|
||||
class MessageAlert {
|
||||
const MessageAlert({
|
||||
required this.messageId,
|
||||
required this.conversationId,
|
||||
required this.senderId,
|
||||
required this.senderName,
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
final String messageId;
|
||||
final String conversationId;
|
||||
final String senderId;
|
||||
final String senderName;
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
|
||||
factory MessageAlert.fromJson(Map<String, dynamic> json) {
|
||||
return MessageAlert(
|
||||
messageId: json['messageId']?.toString() ?? '',
|
||||
conversationId: json['conversationId']?.toString() ?? '',
|
||||
senderId: json['senderId']?.toString() ?? '',
|
||||
senderName: json['senderName']?.toString() ?? 'Utilisateur',
|
||||
content: json['content']?.toString() ?? '',
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.parse(json['timestamp'].toString())
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mise à jour de présence utilisateur.
|
||||
class PresenceUpdate {
|
||||
const PresenceUpdate({
|
||||
required this.userId,
|
||||
required this.isOnline,
|
||||
this.lastSeen,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final bool isOnline;
|
||||
final DateTime? lastSeen;
|
||||
final DateTime timestamp;
|
||||
|
||||
factory PresenceUpdate.fromJson(Map<String, dynamic> json) {
|
||||
return PresenceUpdate(
|
||||
userId: json['userId']?.toString() ?? '',
|
||||
isOnline: json['isOnline'] as bool? ?? false,
|
||||
lastSeen: json['lastSeen'] != null && json['lastSeen'].toString().isNotEmpty
|
||||
? DateTime.tryParse(json['lastSeen'].toString())
|
||||
: null,
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int)
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// Classe SecureStorage pour gérer les opérations de stockage sécurisé.
|
||||
/// Toutes les actions sont loguées pour permettre une traçabilité complète dans le terminal.
|
||||
@@ -7,18 +8,15 @@ class SecureStorage {
|
||||
// Instance de FlutterSecureStorage pour le stockage sécurisé.
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
// Logger pour suivre et enregistrer les actions dans le terminal.
|
||||
final Logger _logger = Logger();
|
||||
|
||||
/// Écrit une valeur dans le stockage sécurisé avec la clé spécifiée.
|
||||
/// Les actions sont loguées et les erreurs sont capturées pour assurer la robustesse.
|
||||
Future<void> write(String key, String value) async {
|
||||
try {
|
||||
_logger.i("[LOG] Tentative d'écriture dans le stockage sécurisé : clé = $key, valeur = $value");
|
||||
AppLogger.d("Tentative d'écriture dans le stockage sécurisé : clé = $key", tag: 'SecureStorage');
|
||||
await _storage.write(key: key, value: value);
|
||||
_logger.i("[LOG] Écriture réussie pour la clé : $key");
|
||||
} catch (e) {
|
||||
_logger.e("[ERROR] Échec d'écriture pour la clé $key : $e");
|
||||
AppLogger.d('Écriture réussie pour la clé : $key', tag: 'SecureStorage');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e("Échec d'écriture pour la clé $key", error: e, stackTrace: stackTrace, tag: 'SecureStorage');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -27,12 +25,12 @@ class SecureStorage {
|
||||
/// Retourne la valeur ou null en cas d'erreur. Chaque action est loguée.
|
||||
Future<String?> read(String key) async {
|
||||
try {
|
||||
_logger.i("[LOG] Lecture de la clé : $key");
|
||||
AppLogger.d('Lecture de la clé : $key', tag: 'SecureStorage');
|
||||
final value = await _storage.read(key: key);
|
||||
_logger.i("[LOG] Valeur lue pour la clé $key : $value");
|
||||
AppLogger.d('Valeur lue pour la clé $key : $value', tag: 'SecureStorage');
|
||||
return value;
|
||||
} catch (e) {
|
||||
_logger.e("[ERROR] Échec de lecture pour la clé $key : $e");
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Échec de lecture pour la clé $key', error: e, stackTrace: stackTrace, tag: 'SecureStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -41,11 +39,11 @@ class SecureStorage {
|
||||
/// Logue chaque étape de l'opération de suppression.
|
||||
Future<void> delete(String key) async {
|
||||
try {
|
||||
_logger.i("[LOG] Suppression de la clé : $key");
|
||||
AppLogger.d('Suppression de la clé : $key', tag: 'SecureStorage');
|
||||
await _storage.delete(key: key);
|
||||
_logger.i("[LOG] Suppression réussie pour la clé : $key");
|
||||
} catch (e) {
|
||||
_logger.e("[ERROR] Échec de suppression pour la clé $key : $e");
|
||||
AppLogger.d('Suppression réussie pour la clé : $key', tag: 'SecureStorage');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('Échec de suppression pour la clé $key', error: e, stackTrace: stackTrace, tag: 'SecureStorage');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -54,62 +52,62 @@ class SecureStorage {
|
||||
/// Logue l'action et assure la robustesse de l'opération.
|
||||
Future<void> saveUserId(String userId) async {
|
||||
if (userId.isNotEmpty) {
|
||||
_logger.i("[LOG] Tentative de sauvegarde de l'userId : $userId");
|
||||
AppLogger.i("Tentative de sauvegarde de l'userId : $userId", tag: 'SecureStorage');
|
||||
await write('user_id', userId);
|
||||
final savedId = await getUserId(); // Récupération immédiate pour vérifier l'enregistrement
|
||||
if (savedId != null && savedId == userId) {
|
||||
_logger.i("[LOG] L'userId a été sauvegardé avec succès et vérifié : $savedId");
|
||||
AppLogger.i("L'userId a été sauvegardé avec succès et vérifié : $savedId", tag: 'SecureStorage');
|
||||
} else {
|
||||
_logger.e("[ERROR] L'userId n'a pas été correctement sauvegardé.");
|
||||
AppLogger.e("L'userId n'a pas été correctement sauvegardé.", tag: 'SecureStorage');
|
||||
}
|
||||
} else {
|
||||
_logger.e("[ERROR] L'userId est vide, échec de sauvegarde.");
|
||||
AppLogger.e("L'userId est vide, échec de sauvegarde.", tag: 'SecureStorage');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère l'identifiant utilisateur depuis le stockage sécurisé.
|
||||
/// Retourne l'ID ou null en cas d'échec.
|
||||
Future<String?> getUserId() async {
|
||||
_logger.i("[LOG] Récupération de l'userId.");
|
||||
return await read('user_id');
|
||||
AppLogger.d("Récupération de l'userId.", tag: 'SecureStorage');
|
||||
return read('user_id');
|
||||
}
|
||||
|
||||
/// Sauvegarde le nom d'utilisateur dans le stockage sécurisé.
|
||||
/// Retourne un booléen pour indiquer le succès ou l'échec.
|
||||
Future<bool> saveUserName(String userName) async {
|
||||
_logger.i("[LOG] Tentative de sauvegarde du userName : $userName");
|
||||
return await _safeWrite('user_name', userName);
|
||||
AppLogger.d('Tentative de sauvegarde du userName : $userName', tag: 'SecureStorage');
|
||||
return _safeWrite('user_name', userName);
|
||||
}
|
||||
|
||||
/// Récupère le nom d'utilisateur depuis le stockage sécurisé.
|
||||
/// Retourne le nom ou null en cas d'échec.
|
||||
Future<String?> getUserName() async {
|
||||
_logger.i("[LOG] Tentative de récupération du userName depuis le stockage sécurisé.");
|
||||
return await _safeRead('user_name');
|
||||
AppLogger.d('Tentative de récupération du userName depuis le stockage sécurisé.', tag: 'SecureStorage');
|
||||
return _safeRead('user_name');
|
||||
}
|
||||
|
||||
/// Sauvegarde le prénom de l'utilisateur dans le stockage sécurisé.
|
||||
/// Retourne un booléen pour indiquer le succès ou l'échec.
|
||||
Future<bool> saveUserLastName(String userLastName) async {
|
||||
_logger.i("[LOG] Tentative de sauvegarde du userLastName : $userLastName");
|
||||
return await _safeWrite('user_last_name', userLastName);
|
||||
AppLogger.d('Tentative de sauvegarde du userLastName : $userLastName', tag: 'SecureStorage');
|
||||
return _safeWrite('user_last_name', userLastName);
|
||||
}
|
||||
|
||||
/// Récupère le prénom de l'utilisateur depuis le stockage sécurisé.
|
||||
/// Retourne le prénom ou null en cas d'échec.
|
||||
Future<String?> getUserLastName() async {
|
||||
_logger.i("[LOG] Tentative de récupération du userLastName depuis le stockage sécurisé.");
|
||||
return await _safeRead('user_last_name');
|
||||
AppLogger.d('Tentative de récupération du userLastName depuis le stockage sécurisé.', tag: 'SecureStorage');
|
||||
return _safeRead('user_last_name');
|
||||
}
|
||||
|
||||
/// Supprime toutes les informations utilisateur du stockage sécurisé.
|
||||
/// Logue chaque étape de la suppression.
|
||||
Future<void> deleteUserInfo() async {
|
||||
_logger.i("[LOG] Tentative de suppression de toutes les informations utilisateur.");
|
||||
AppLogger.i('Tentative de suppression de toutes les informations utilisateur.', tag: 'SecureStorage');
|
||||
await delete('user_id');
|
||||
await delete('user_name');
|
||||
await delete('user_last_name');
|
||||
_logger.i("[LOG] Suppression réussie des informations utilisateur.");
|
||||
AppLogger.i('Suppression réussie des informations utilisateur.', tag: 'SecureStorage');
|
||||
}
|
||||
|
||||
/// Méthode privée pour encapsuler l'écriture sécurisée avec gestion d'erreur.
|
||||
@@ -119,7 +117,7 @@ class SecureStorage {
|
||||
await write(key, value);
|
||||
return true; // Indique que l'écriture a réussi.
|
||||
} catch (e) {
|
||||
_logger.e("[ERROR] Erreur lors de l'écriture sécurisée : $e");
|
||||
AppLogger.e("Erreur lors de l'écriture sécurisée", error: e, tag: 'SecureStorage');
|
||||
return false; // Indique un échec.
|
||||
}
|
||||
}
|
||||
@@ -130,7 +128,7 @@ class SecureStorage {
|
||||
try {
|
||||
return await read(key);
|
||||
} catch (e) {
|
||||
_logger.e("[ERROR] Erreur lors de la lecture sécurisée : $e");
|
||||
AppLogger.e('Erreur lors de la lecture sécurisée', error: e, tag: 'SecureStorage');
|
||||
return null; // Retourne null en cas d'erreur.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user