feat(frontend): Séparation des demandes d'amitié envoyées et reçues
- Ajout de deux endpoints distincts dans Urls: getSentFriendRequestsWithUserId et getReceivedFriendRequestsWithUserId - Ajout de méthodes dans FriendsRepository et FriendsRepositoryImpl pour récupérer séparément les demandes envoyées et reçues - Ajout de la méthode cancelFriendRequest pour annuler une demande envoyée - Modification de FriendsProvider pour gérer deux listes distinctes: sentRequests et receivedRequests - Mise à jour de FriendsScreen pour afficher deux sections: - Demandes reçues: avec boutons Accepter/Rejeter - Demandes envoyées: avec bouton Annuler uniquement - Correction du mapping JSON dans FriendRequest.fromJson (userNom/userPrenoms correctement mappés) - Amélioration de FriendRequestCard pour gérer les deux types de demandes - Correction de la validation d'URL d'image dans FriendDetailScreen - Support du champ uuid dans UserModel.fromJson pour compatibilité backend
This commit is contained in:
@@ -1,32 +1,374 @@
|
|||||||
|
import 'env_config.dart';
|
||||||
|
|
||||||
|
/// Classe utilitaire pour gérer toutes les URLs de l'API backend.
|
||||||
|
///
|
||||||
|
/// Cette classe centralise toutes les URLs de l'API pour faciliter
|
||||||
|
/// la maintenance et éviter la duplication de code.
|
||||||
|
///
|
||||||
|
/// **Usage:**
|
||||||
|
/// ```dart
|
||||||
|
/// // URL simple
|
||||||
|
/// final url = Urls.authenticateUser;
|
||||||
|
///
|
||||||
|
/// // URL avec paramètres dynamiques
|
||||||
|
/// final userUrl = Urls.getUserByIdWithId('123');
|
||||||
|
/// final eventUrl = Urls.getEventByIdWithId('456');
|
||||||
|
/// ```
|
||||||
class Urls {
|
class Urls {
|
||||||
static const String baseUrl = 'http://192.168.1.16:8080';
|
/// Constructeur privé pour empêcher l'instanciation
|
||||||
|
Urls._();
|
||||||
|
|
||||||
// Authentication and Users Endpoints
|
/// URL de base de l'API
|
||||||
static const String authenticateUser = '$baseUrl/users/authenticate';
|
static String get baseUrl => EnvConfig.apiBaseUrl;
|
||||||
static const String createUser = '$baseUrl/users';
|
|
||||||
static const String getUserById = '$baseUrl/users'; // Append '/{id}' dynamically
|
|
||||||
static const String deleteUser = '$baseUrl/users'; // Append '/{id}' dynamically
|
|
||||||
static const String updateUserProfileImage = '$baseUrl/users'; // Append '/{id}/profile-image' dynamically
|
|
||||||
|
|
||||||
// Events Endpoints
|
// ============================================================================
|
||||||
static const String createEvent = '$baseUrl/events';
|
// AUTHENTIFICATION ET UTILISATEURS
|
||||||
static const String getEventById = '$baseUrl/events'; // Append '/{id}' dynamically
|
// ============================================================================
|
||||||
static const String deleteEvent = '$baseUrl/events'; // Append '/{id}' dynamically
|
|
||||||
static const String getEventsAfterDate = '$baseUrl/events/after-date';
|
|
||||||
static const String addParticipant = '$baseUrl/events'; // Append '/{id}/participants' dynamically
|
|
||||||
static const String removeParticipant = '$baseUrl/events'; // Append '/{id}/participants/{userId}' dynamically
|
|
||||||
static const String getNumberOfParticipants = '$baseUrl/events'; // Append '/{id}/participants/count' dynamically
|
|
||||||
static const String closeEvent = '$baseUrl/events'; // Append '/{id}/close' dynamically
|
|
||||||
static const String reopenEvent = '$baseUrl/events'; // Append '/{id}/reopen' dynamically
|
|
||||||
static const String updateEvent = '$baseUrl/events'; // Append '/{id}' dynamically
|
|
||||||
static const String updateEventImage = '$baseUrl/events'; // Append '/{id}/image' dynamically
|
|
||||||
static const String getAllEvents = '$baseUrl/events';
|
|
||||||
static const String getEventsByCategory = '$baseUrl/events/category'; // Append '/{category}' dynamically
|
|
||||||
static const String updateEventStatus = '$baseUrl/events'; // Append '/{id}/status' dynamically
|
|
||||||
static const String searchEvents = '$baseUrl/events/search'; // Use query parameter for 'keyword'
|
|
||||||
static const String getEventsByUser = '$baseUrl/events/user'; // Append '/{userId}' dynamically
|
|
||||||
static const String getEventsByStatus = '$baseUrl/events/status'; // Append '/{status}' dynamically
|
|
||||||
static const String getEventsBetweenDates = '$baseUrl/events/between-dates'; // Use query parameters for startDate and endDate
|
|
||||||
|
|
||||||
// Other URLs can be added here as the project expands
|
/// Endpoint pour authentifier un utilisateur
|
||||||
|
static String get authenticateUser => '$baseUrl/users/authenticate';
|
||||||
|
|
||||||
|
/// Endpoint pour créer un nouvel utilisateur
|
||||||
|
static String get createUser => '$baseUrl/users';
|
||||||
|
|
||||||
|
/// Endpoint de base pour les opérations sur les utilisateurs
|
||||||
|
static String get usersBase => '$baseUrl/users';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir un utilisateur par son ID
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String getUserByIdWithId(String userId) => '$usersBase/$userId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour rechercher un utilisateur par email
|
||||||
|
///
|
||||||
|
/// [email] L'email de l'utilisateur à rechercher
|
||||||
|
static String searchUserByEmail(String email) =>
|
||||||
|
'$usersBase/search?email=${Uri.encodeComponent(email)}';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour supprimer un utilisateur par son ID
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String deleteUserWithId(String userId) => '$usersBase/$userId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour mettre à jour l'image de profil d'un utilisateur
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String updateUserProfileImageWithId(String userId) =>
|
||||||
|
'$usersBase/$userId/profile-image';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour mettre à jour un utilisateur
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String updateUserWithId(String userId) => '$usersBase/$userId';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ÉVÉNEMENTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Endpoint de base pour les opérations sur les événements
|
||||||
|
static String get eventsBase => '$baseUrl/events';
|
||||||
|
|
||||||
|
/// Endpoint pour créer un nouvel événement
|
||||||
|
static String get createEvent => eventsBase;
|
||||||
|
|
||||||
|
/// Endpoint pour obtenir tous les événements
|
||||||
|
static String get getAllEvents => eventsBase;
|
||||||
|
|
||||||
|
/// Endpoint pour obtenir les événements créés par un utilisateur et ses amis
|
||||||
|
static String get getEventsCreatedByUserAndFriends =>
|
||||||
|
'$eventsBase/created-by-user-and-friends';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir un événement par son ID
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String getEventByIdWithId(String eventId) => '$eventsBase/$eventId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour supprimer un événement par son ID
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String deleteEventWithId(String eventId) => '$eventsBase/$eventId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour mettre à jour un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String updateEventWithId(String eventId) => '$eventsBase/$eventId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour mettre à jour l'image d'un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String updateEventImageWithId(String eventId) =>
|
||||||
|
'$eventsBase/$eventId/image';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour fermer un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String closeEventWithId(String eventId) => '$eventsBase/$eventId/close';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour rouvrir un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String reopenEventWithId(String eventId) =>
|
||||||
|
'$eventsBase/$eventId/reopen';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour mettre à jour le statut d'un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String updateEventStatusWithId(String eventId) =>
|
||||||
|
'$eventsBase/$eventId/status';
|
||||||
|
|
||||||
|
/// Endpoint pour obtenir les événements après une date
|
||||||
|
static String get getEventsAfterDate => '$eventsBase/after-date';
|
||||||
|
|
||||||
|
/// Endpoint pour obtenir les événements entre deux dates
|
||||||
|
static String get getEventsBetweenDates => '$eventsBase/between-dates';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir les événements par catégorie
|
||||||
|
///
|
||||||
|
/// [category] La catégorie des événements
|
||||||
|
static String getEventsByCategoryWithCategory(String category) =>
|
||||||
|
'$eventsBase/category/$category';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir les événements par statut
|
||||||
|
///
|
||||||
|
/// [status] Le statut des événements (ouvert, fermé, etc.)
|
||||||
|
static String getEventsByStatusWithStatus(String status) =>
|
||||||
|
'$eventsBase/status/$status';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir les événements d'un utilisateur
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String getEventsByUserWithUserId(String userId) =>
|
||||||
|
'$eventsBase/user/$userId';
|
||||||
|
|
||||||
|
/// Endpoint pour rechercher des événements
|
||||||
|
///
|
||||||
|
/// **Note:** Utilisez des paramètres de requête pour le mot-clé
|
||||||
|
static String get searchEvents => '$eventsBase/search';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PARTICIPANTS AUX ÉVÉNEMENTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Retourne l'URL pour ajouter un participant à un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String addParticipantWithEventId(String eventId) =>
|
||||||
|
'$eventsBase/$eventId/participants';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour retirer un participant d'un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
/// [userId] L'ID de l'utilisateur à retirer
|
||||||
|
static String removeParticipantWithIds(String eventId, String userId) =>
|
||||||
|
'$eventsBase/$eventId/participants/$userId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir le nombre de participants d'un événement
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String getNumberOfParticipantsWithEventId(String eventId) =>
|
||||||
|
'$eventsBase/$eventId/participants/count';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour réagir à un événement (utilise favorite)
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String reactToEventWithId(String eventId, String userId) =>
|
||||||
|
'$eventsBase/$eventId/favorite?userId=$userId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour participer à un événement (utilise participants)
|
||||||
|
///
|
||||||
|
/// [eventId] L'ID de l'événement
|
||||||
|
static String participateInEventWithId(String eventId) =>
|
||||||
|
'$eventsBase/$eventId/participants';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AMIS ET RELATIONS SOCIALES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Endpoint de base pour les opérations sur les amis
|
||||||
|
static String get friendsBase => '$baseUrl/friends';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir les amis d'un utilisateur
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String getFriendsWithUserId(String userId) =>
|
||||||
|
'$friendsBase/user/$userId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour ajouter un ami
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
/// [friendId] L'ID de l'ami à ajouter
|
||||||
|
static String addFriendWithIds(String userId, String friendId) =>
|
||||||
|
'$friendsBase/$userId/$friendId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour supprimer un ami
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
/// [friendId] L'ID de l'ami à supprimer
|
||||||
|
static String removeFriendWithIds(String userId, String friendId) =>
|
||||||
|
'$friendsBase/$userId/$friendId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour récupérer les demandes d'amitié en attente
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
/// [page] Le numéro de la page (optionnel, par défaut 0)
|
||||||
|
/// [size] La taille de la page (optionnel, par défaut 10)
|
||||||
|
static String getPendingFriendRequestsWithUserId(String userId, {int page = 0, int size = 10}) =>
|
||||||
|
'$friendsBase/pending/$userId?page=$page&size=$size';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour récupérer les demandes d'amitié envoyées
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
/// [page] Le numéro de la page (optionnel, par défaut 0)
|
||||||
|
/// [size] La taille de la page (optionnel, par défaut 10)
|
||||||
|
static String getSentFriendRequestsWithUserId(String userId, {int page = 0, int size = 10}) =>
|
||||||
|
'$friendsBase/sent/$userId?page=$page&size=$size';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour récupérer les demandes d'amitié reçues
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
/// [page] Le numéro de la page (optionnel, par défaut 0)
|
||||||
|
/// [size] La taille de la page (optionnel, par défaut 10)
|
||||||
|
static String getReceivedFriendRequestsWithUserId(String userId, {int page = 0, int size = 10}) =>
|
||||||
|
'$friendsBase/received/$userId?page=$page&size=$size';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour accepter une demande d'amitié
|
||||||
|
///
|
||||||
|
/// [friendshipId] L'ID de la relation d'amitié
|
||||||
|
static String acceptFriendRequestWithId(String friendshipId) =>
|
||||||
|
'$friendsBase/$friendshipId/accept';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour rejeter une demande d'amitié
|
||||||
|
///
|
||||||
|
/// [friendshipId] L'ID de la relation d'amitié
|
||||||
|
static String rejectFriendRequestWithId(String friendshipId) =>
|
||||||
|
'$friendsBase/$friendshipId/reject';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NOTIFICATIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Endpoint de base pour les opérations sur les notifications
|
||||||
|
static String get notificationsBase => '$baseUrl/notifications';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir les notifications d'un utilisateur
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String getNotificationsWithUserId(String userId) =>
|
||||||
|
'$notificationsBase/user/$userId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour marquer une notification comme lue
|
||||||
|
///
|
||||||
|
/// [notificationId] L'ID de la notification
|
||||||
|
static String markNotificationAsReadWithId(String notificationId) =>
|
||||||
|
'$notificationsBase/$notificationId/read';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour marquer toutes les notifications comme lues
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String markAllNotificationsAsReadWithUserId(String userId) =>
|
||||||
|
'$notificationsBase/user/$userId/mark-all-read';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour supprimer une notification
|
||||||
|
///
|
||||||
|
/// [notificationId] L'ID de la notification
|
||||||
|
static String deleteNotificationWithId(String notificationId) =>
|
||||||
|
'$notificationsBase/$notificationId';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// POSTS SOCIAUX
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Endpoint de base pour les opérations sur les posts sociaux
|
||||||
|
static String get postsBase => '$baseUrl/posts';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir tous les posts (avec pagination)
|
||||||
|
static String get getAllPosts => postsBase;
|
||||||
|
|
||||||
|
/// Retourne l'URL pour créer un nouveau post
|
||||||
|
static String get createSocialPost => postsBase;
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir un post par son ID
|
||||||
|
///
|
||||||
|
/// [postId] L'ID du post
|
||||||
|
static String getSocialPostByIdWithId(String postId) => '$postsBase/$postId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour mettre à jour un post
|
||||||
|
///
|
||||||
|
/// [postId] L'ID du post
|
||||||
|
static String updateSocialPostWithId(String postId) => '$postsBase/$postId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour supprimer un post
|
||||||
|
///
|
||||||
|
/// [postId] L'ID du post
|
||||||
|
static String deleteSocialPostWithId(String postId) => '$postsBase/$postId';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour rechercher des posts
|
||||||
|
///
|
||||||
|
/// [query] Le terme de recherche
|
||||||
|
static String searchSocialPostsWithQuery(String query) =>
|
||||||
|
'$postsBase/search?q=${Uri.encodeComponent(query)}';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour liker un post
|
||||||
|
///
|
||||||
|
/// [postId] L'ID du post
|
||||||
|
static String likeSocialPostWithId(String postId) => '$postsBase/$postId/like';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour commenter un post
|
||||||
|
///
|
||||||
|
/// [postId] L'ID du post
|
||||||
|
static String commentSocialPostWithId(String postId) =>
|
||||||
|
'$postsBase/$postId/comment';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour partager un post
|
||||||
|
///
|
||||||
|
/// [postId] L'ID du post
|
||||||
|
static String shareSocialPostWithId(String postId) => '$postsBase/$postId/share';
|
||||||
|
|
||||||
|
/// Retourne l'URL pour obtenir les posts d'un utilisateur
|
||||||
|
///
|
||||||
|
/// [userId] L'ID de l'utilisateur
|
||||||
|
static String getSocialPostsByUserId(String userId) =>
|
||||||
|
'$postsBase/user/$userId';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MÉTHODES UTILITAIRES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Construit une URL avec des paramètres de requête
|
||||||
|
///
|
||||||
|
/// [baseUrl] L'URL de base
|
||||||
|
/// [params] Les paramètres de requête (clé-valeur)
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final url = Urls.buildUrlWithParams(
|
||||||
|
/// Urls.searchEvents,
|
||||||
|
/// {'keyword': 'concert', 'category': 'music'},
|
||||||
|
/// );
|
||||||
|
/// // Résultat: 'http://api.com/events/search?keyword=concert&category=music'
|
||||||
|
/// ```
|
||||||
|
static String buildUrlWithParams(String baseUrl, Map<String, String> params) {
|
||||||
|
if (params.isEmpty) return baseUrl;
|
||||||
|
|
||||||
|
final queryString = params.entries
|
||||||
|
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
|
||||||
|
.join('&');
|
||||||
|
|
||||||
|
return '$baseUrl?$queryString';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valide qu'une URL est bien formée
|
||||||
|
///
|
||||||
|
/// [url] L'URL à valider
|
||||||
|
///
|
||||||
|
/// Returns `true` si l'URL est valide, `false` sinon
|
||||||
|
static bool isValidUrl(String url) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
return uri.hasScheme && uri.hasAuthority;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,223 @@
|
|||||||
|
import '../../core/constants/env_config.dart';
|
||||||
import '../../domain/entities/user.dart';
|
import '../../domain/entities/user.dart';
|
||||||
|
|
||||||
/// Modèle représentant l'utilisateur dans l'application AfterWork.
|
/// Modèle de données pour les utilisateurs (Data Transfer Object).
|
||||||
/// Ce modèle est utilisé pour la conversion JSON et l'interaction avec l'API.
|
///
|
||||||
|
/// Cette classe est responsable de la sérialisation/désérialisation
|
||||||
|
/// avec l'API backend et convertit vers/depuis l'entité de domaine [User].
|
||||||
|
///
|
||||||
|
/// **Usage:**
|
||||||
|
/// ```dart
|
||||||
|
/// // Depuis JSON
|
||||||
|
/// final user = UserModel.fromJson(jsonData);
|
||||||
|
///
|
||||||
|
/// // Vers JSON
|
||||||
|
/// final json = user.toJson();
|
||||||
|
///
|
||||||
|
/// // Vers entité de domaine
|
||||||
|
/// final entity = user.toEntity();
|
||||||
|
/// ```
|
||||||
class UserModel extends User {
|
class UserModel extends User {
|
||||||
UserModel({
|
/// Crée une nouvelle instance de [UserModel].
|
||||||
required String userId,
|
///
|
||||||
required String userLastName,
|
/// [userId] L'identifiant unique de l'utilisateur
|
||||||
required String userFirstName,
|
/// [userLastName] Le nom de famille de l'utilisateur
|
||||||
required String email,
|
/// [userFirstName] Le prénom de l'utilisateur
|
||||||
required String motDePasse,
|
/// [email] L'adresse email de l'utilisateur
|
||||||
required String profileImageUrl,
|
/// [motDePasse] Le mot de passe (hashé côté serveur)
|
||||||
}) : super(
|
/// [profileImageUrl] L'URL de l'image de profil
|
||||||
|
/// [eventsCount] Le nombre d'événements créés (optionnel)
|
||||||
|
/// [friendsCount] Le nombre d'amis (optionnel)
|
||||||
|
/// [postsCount] Le nombre de posts (optionnel)
|
||||||
|
/// [visitedPlacesCount] Le nombre de lieux visités (optionnel)
|
||||||
|
const UserModel({
|
||||||
|
required super.userId,
|
||||||
|
required super.userLastName,
|
||||||
|
required super.userFirstName,
|
||||||
|
required super.email,
|
||||||
|
required super.motDePasse,
|
||||||
|
required super.profileImageUrl,
|
||||||
|
super.eventsCount,
|
||||||
|
super.friendsCount,
|
||||||
|
super.postsCount,
|
||||||
|
super.visitedPlacesCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Crée un [UserModel] à partir d'un JSON reçu depuis l'API.
|
||||||
|
///
|
||||||
|
/// [json] Les données JSON à parser
|
||||||
|
///
|
||||||
|
/// Returns un [UserModel] avec les données parsées
|
||||||
|
///
|
||||||
|
/// **Note:** Les valeurs par défaut sont utilisées si des champs sont manquants.
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final json = {
|
||||||
|
/// 'userId': '123',
|
||||||
|
/// 'nom': 'Doe',
|
||||||
|
/// 'prenoms': 'John',
|
||||||
|
/// 'email': 'john@example.com',
|
||||||
|
/// };
|
||||||
|
/// final user = UserModel.fromJson(json);
|
||||||
|
/// ```
|
||||||
|
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
try {
|
||||||
|
// Le backend peut renvoyer 'uuid' ou 'userId', on accepte les deux
|
||||||
|
final userId = _parseString(json, 'userId', '') != ''
|
||||||
|
? _parseString(json, 'userId', '')
|
||||||
|
: _parseString(json, 'uuid', '');
|
||||||
|
|
||||||
|
return UserModel(
|
||||||
|
userId: userId.isNotEmpty ? userId : (json['uuid']?.toString() ?? json['userId']?.toString() ?? ''),
|
||||||
|
userLastName: _parseString(json, 'nom', 'Inconnu'),
|
||||||
|
userFirstName: _parseString(json, 'prenoms', 'Inconnu'),
|
||||||
|
email: _parseString(json, 'email', ''),
|
||||||
|
motDePasse: _parseString(json, 'motDePasse', ''),
|
||||||
|
profileImageUrl: _parseString(json, 'profileImageUrl', ''),
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse une valeur string depuis le JSON avec valeur par défaut.
|
||||||
|
static String _parseString(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
String key,
|
||||||
|
String defaultValue,
|
||||||
|
) {
|
||||||
|
final value = json[key];
|
||||||
|
if (value == null) return defaultValue;
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse une valeur int depuis le JSON (optionnel).
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convertit ce [UserModel] en JSON pour l'envoi vers l'API.
|
||||||
|
///
|
||||||
|
/// Returns une [Map] contenant les données de l'utilisateur
|
||||||
|
///
|
||||||
|
/// **Note:** Le mot de passe est envoyé en clair (hashé côté serveur).
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final user = UserModel(...);
|
||||||
|
/// final json = user.toJson();
|
||||||
|
/// // Envoyer json à l'API
|
||||||
|
/// ```
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{
|
||||||
|
if (userId.isNotEmpty) 'id': userId,
|
||||||
|
'nom': userLastName,
|
||||||
|
'prenoms': userFirstName,
|
||||||
|
'email': email,
|
||||||
|
if (motDePasse.isNotEmpty) 'motDePasse': motDePasse,
|
||||||
|
if (profileImageUrl.isNotEmpty) 'profileImageUrl': profileImageUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter les compteurs optionnels s'ils sont présents
|
||||||
|
if (eventsCount != null) {
|
||||||
|
json['eventsCount'] = eventsCount;
|
||||||
|
}
|
||||||
|
if (friendsCount != null) {
|
||||||
|
json['friendsCount'] = friendsCount;
|
||||||
|
}
|
||||||
|
if (postsCount != null) {
|
||||||
|
json['postsCount'] = postsCount;
|
||||||
|
}
|
||||||
|
if (visitedPlacesCount != null) {
|
||||||
|
json['visitedPlacesCount'] = visitedPlacesCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convertit ce modèle vers une entité de domaine [User].
|
||||||
|
///
|
||||||
|
/// Returns une instance de [User] avec les mêmes données
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final model = UserModel.fromJson(json);
|
||||||
|
/// final entity = model.toEntity();
|
||||||
|
/// ```
|
||||||
|
User toEntity() {
|
||||||
|
return User(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
userLastName: userLastName,
|
userLastName: userLastName,
|
||||||
userFirstName: userFirstName,
|
userFirstName: userFirstName,
|
||||||
email: email,
|
email: email,
|
||||||
motDePasse: motDePasse,
|
motDePasse: motDePasse,
|
||||||
profileImageUrl: profileImageUrl,
|
profileImageUrl: profileImageUrl,
|
||||||
|
eventsCount: eventsCount,
|
||||||
|
friendsCount: friendsCount,
|
||||||
|
postsCount: postsCount,
|
||||||
|
visitedPlacesCount: visitedPlacesCount,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Factory pour créer un `UserModel` à partir d'un JSON reçu depuis l'API.
|
/// Crée une copie de ce [UserModel] avec des valeurs modifiées.
|
||||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
///
|
||||||
|
/// Tous les paramètres sont optionnels. Seuls les paramètres fournis
|
||||||
|
/// seront modifiés dans la nouvelle instance.
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final updated = user.copyWith(
|
||||||
|
/// userFirstName: 'Jane',
|
||||||
|
/// profileImageUrl: 'https://example.com/new-image.jpg',
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
UserModel copyWith({
|
||||||
|
String? userId,
|
||||||
|
String? userLastName,
|
||||||
|
String? userFirstName,
|
||||||
|
String? email,
|
||||||
|
String? motDePasse,
|
||||||
|
String? profileImageUrl,
|
||||||
|
int? eventsCount,
|
||||||
|
int? friendsCount,
|
||||||
|
int? postsCount,
|
||||||
|
int? visitedPlacesCount,
|
||||||
|
}) {
|
||||||
return UserModel(
|
return UserModel(
|
||||||
userId: json['userId'] ?? '',
|
userId: userId ?? this.userId,
|
||||||
userLastName: json['nom'] ?? 'Inconnu',
|
userLastName: userLastName ?? this.userLastName,
|
||||||
userFirstName: json['prenoms'] ?? 'Inconnu',
|
userFirstName: userFirstName ?? this.userFirstName,
|
||||||
email: json['email'] ?? 'inconnu@example.com',
|
email: email ?? this.email,
|
||||||
motDePasse: json['motDePasse'] ?? '',
|
motDePasse: motDePasse ?? this.motDePasse,
|
||||||
profileImageUrl: json['profileImageUrl'] ?? '',
|
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
|
||||||
|
eventsCount: eventsCount ?? this.eventsCount,
|
||||||
|
friendsCount: friendsCount ?? this.friendsCount,
|
||||||
|
postsCount: postsCount ?? this.postsCount,
|
||||||
|
visitedPlacesCount: visitedPlacesCount ?? this.visitedPlacesCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convertit le `UserModel` en JSON pour l'envoi vers l'API.
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
String toString() {
|
||||||
return {
|
return 'UserModel('
|
||||||
'id': userId,
|
'userId: $userId, '
|
||||||
'nom': userLastName,
|
'name: $userFirstName $userLastName, '
|
||||||
'prenoms': userFirstName,
|
'email: $email'
|
||||||
'email': email,
|
')';
|
||||||
'motDePasse': motDePasse, // Mot de passe en clair (comme demandé temporairement)
|
|
||||||
'profileImageUrl':profileImageUrl,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import '../../domain/entities/friend.dart';
|
|
||||||
import '../../data/repositories/friends_repository_impl.dart';
|
import '../../data/repositories/friends_repository_impl.dart';
|
||||||
|
import '../../data/services/secure_storage.dart';
|
||||||
|
import '../../domain/entities/friend.dart';
|
||||||
|
import '../../domain/entities/friend_request.dart';
|
||||||
|
|
||||||
/// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis.
|
/// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis.
|
||||||
/// Il interagit avec le [FriendsRepositoryImpl] pour effectuer des appels API et gérer
|
/// Il interagit avec le [FriendsRepositoryImpl] pour effectuer des appels API et gérer
|
||||||
/// la liste des amis de l'utilisateur, avec une gestion avancée de la pagination,
|
/// la liste des amis de l'utilisateur, avec une gestion avancée de la pagination,
|
||||||
/// du statut des amis et de la gestion des erreurs.
|
/// du statut des amis et de la gestion des erreurs.
|
||||||
class FriendsProvider with ChangeNotifier {
|
class FriendsProvider with ChangeNotifier { // Nombre d'amis à récupérer par page
|
||||||
|
|
||||||
|
/// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl].
|
||||||
|
FriendsProvider({required this.friendsRepository});
|
||||||
final FriendsRepositoryImpl friendsRepository;
|
final FriendsRepositoryImpl friendsRepository;
|
||||||
final Logger _logger = Logger(); // Utilisation du logger pour une traçabilité complète des actions.
|
final Logger _logger = Logger(); // Utilisation du logger pour une traçabilité complète des actions.
|
||||||
|
|
||||||
@@ -16,15 +22,32 @@ class FriendsProvider with ChangeNotifier {
|
|||||||
bool _isLoading = false; // Indicateur de chargement
|
bool _isLoading = false; // Indicateur de chargement
|
||||||
bool _hasMore = true; // Indicateur de pagination
|
bool _hasMore = true; // Indicateur de pagination
|
||||||
int _currentPage = 0; // Numéro de la page actuelle pour la pagination
|
int _currentPage = 0; // Numéro de la page actuelle pour la pagination
|
||||||
final int _friendsPerPage = 10; // Nombre d'amis à récupérer par page
|
final int _friendsPerPage = 10;
|
||||||
|
|
||||||
/// Constructeur de [FriendsProvider] qui nécessite l'instance d'un [FriendsRepositoryImpl].
|
// Liste des demandes d'amitié envoyées
|
||||||
FriendsProvider({required this.friendsRepository});
|
List<FriendRequest> _sentRequests = [];
|
||||||
|
bool _isLoadingSentRequests = false;
|
||||||
|
int _currentSentRequestPage = 0;
|
||||||
|
|
||||||
|
// Liste des demandes d'amitié reçues
|
||||||
|
List<FriendRequest> _receivedRequests = [];
|
||||||
|
bool _isLoadingReceivedRequests = false;
|
||||||
|
int _currentReceivedRequestPage = 0;
|
||||||
|
|
||||||
|
final int _requestsPerPage = 10;
|
||||||
|
|
||||||
// Getters pour accéder à l'état actuel des données
|
// Getters pour accéder à l'état actuel des données
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
bool get hasMore => _hasMore;
|
bool get hasMore => _hasMore;
|
||||||
List<Friend> get friendsList => _friendsList;
|
List<Friend> get friendsList => _friendsList;
|
||||||
|
List<FriendRequest> get sentRequests => _sentRequests;
|
||||||
|
List<FriendRequest> get receivedRequests => _receivedRequests;
|
||||||
|
bool get isLoadingSentRequests => _isLoadingSentRequests;
|
||||||
|
bool get isLoadingReceivedRequests => _isLoadingReceivedRequests;
|
||||||
|
|
||||||
|
// Pour compatibilité avec l'ancien code
|
||||||
|
List<FriendRequest> get pendingRequests => _receivedRequests;
|
||||||
|
bool get isLoadingRequests => _isLoadingReceivedRequests;
|
||||||
|
|
||||||
/// Récupère la liste des amis pour un utilisateur donné avec pagination.
|
/// Récupère la liste des amis pour un utilisateur donné avec pagination.
|
||||||
///
|
///
|
||||||
@@ -63,10 +86,10 @@ class FriendsProvider with ChangeNotifier {
|
|||||||
_logger.i('[LOG] Plus d\'amis à charger.');
|
_logger.i('[LOG] Plus d\'amis à charger.');
|
||||||
} else {
|
} else {
|
||||||
// Ajout des amis à la liste, en excluant l'utilisateur connecté
|
// Ajout des amis à la liste, en excluant l'utilisateur connecté
|
||||||
for (var friend in newFriends) {
|
for (final friend in newFriends) {
|
||||||
if (friend.friendId != userId) {
|
if (friend.friendId != userId) {
|
||||||
_friendsList.add(friend);
|
_friendsList.add(friend);
|
||||||
_logger.i("[LOG] Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}");
|
_logger.i('[LOG] Ami ajouté : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}');
|
||||||
} else {
|
} else {
|
||||||
_logger.w("[WARN] L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}");
|
_logger.w("[WARN] L'utilisateur connecté est exclu de la liste des amis : ${friend.friendId}");
|
||||||
}
|
}
|
||||||
@@ -171,4 +194,192 @@ class FriendsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ajoute un nouvel ami.
|
||||||
|
///
|
||||||
|
/// [friendId] : L'identifiant unique de l'ami à ajouter.
|
||||||
|
///
|
||||||
|
/// Cette méthode :
|
||||||
|
/// - Loggue chaque étape.
|
||||||
|
/// - Envoie la demande d'ami via l'API.
|
||||||
|
/// - Rafraîchit la liste des amis si l'ajout réussit.
|
||||||
|
Future<void> addFriend(String friendId) async {
|
||||||
|
try {
|
||||||
|
// Récupérer le userId de l'utilisateur actuel
|
||||||
|
final currentUserId = await _getCurrentUserId();
|
||||||
|
if (currentUserId == null || currentUserId.isEmpty) {
|
||||||
|
throw Exception('Utilisateur non connecté');
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.i('[LOG] Ajout de l\'ami: userId=$currentUserId, friendId=$friendId');
|
||||||
|
await friendsRepository.addFriend(currentUserId, friendId);
|
||||||
|
_logger.i('[LOG] Demande d\'ami envoyée avec succès');
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
rethrow; // Propager l'erreur pour que l'UI puisse l'afficher
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère l'ID de l'utilisateur actuel depuis le stockage sécurisé
|
||||||
|
Future<String?> _getCurrentUserId() async {
|
||||||
|
try {
|
||||||
|
final secureStorage = SecureStorage();
|
||||||
|
return await secureStorage.getUserId();
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('[ERROR] Erreur lors de la récupération de l\'userId : $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié en attente pour l'utilisateur actuel (compatibilité).
|
||||||
|
Future<void> fetchPendingRequests({bool loadMore = false}) async {
|
||||||
|
await fetchReceivedRequests(loadMore: loadMore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié envoyées par l'utilisateur actuel.
|
||||||
|
Future<void> fetchSentRequests({bool loadMore = false}) async {
|
||||||
|
try {
|
||||||
|
final currentUserId = await _getCurrentUserId();
|
||||||
|
if (currentUserId == null || currentUserId.isEmpty) {
|
||||||
|
throw Exception('Utilisateur non connecté');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loadMore) {
|
||||||
|
_currentSentRequestPage = 0;
|
||||||
|
_sentRequests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoadingSentRequests = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final page = loadMore ? _currentSentRequestPage + 1 : 0;
|
||||||
|
final requests = await friendsRepository.getSentFriendRequests(
|
||||||
|
currentUserId,
|
||||||
|
page,
|
||||||
|
_requestsPerPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadMore) {
|
||||||
|
_sentRequests.addAll(requests);
|
||||||
|
_currentSentRequestPage = page;
|
||||||
|
} else {
|
||||||
|
_sentRequests = requests;
|
||||||
|
_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');
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
_isLoadingSentRequests = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié reçues par l'utilisateur actuel.
|
||||||
|
Future<void> fetchReceivedRequests({bool loadMore = false}) async {
|
||||||
|
try {
|
||||||
|
final currentUserId = await _getCurrentUserId();
|
||||||
|
if (currentUserId == null || currentUserId.isEmpty) {
|
||||||
|
throw Exception('Utilisateur non connecté');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loadMore) {
|
||||||
|
_currentReceivedRequestPage = 0;
|
||||||
|
_receivedRequests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoadingReceivedRequests = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final page = loadMore ? _currentReceivedRequestPage + 1 : 0;
|
||||||
|
final requests = await friendsRepository.getReceivedFriendRequests(
|
||||||
|
currentUserId,
|
||||||
|
page,
|
||||||
|
_requestsPerPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadMore) {
|
||||||
|
_receivedRequests.addAll(requests);
|
||||||
|
_currentReceivedRequestPage = page;
|
||||||
|
} else {
|
||||||
|
_receivedRequests = requests;
|
||||||
|
_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');
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
_isLoadingReceivedRequests = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepte une demande d'amitié.
|
||||||
|
Future<void> acceptFriendRequest(String friendshipId) async {
|
||||||
|
try {
|
||||||
|
_logger.i('[LOG] Acceptation de la demande d\'amitié: $friendshipId');
|
||||||
|
await friendsRepository.acceptFriendRequest(friendshipId);
|
||||||
|
|
||||||
|
// Retirer la demande de la liste des demandes reçues
|
||||||
|
_receivedRequests.removeWhere((req) => req.friendshipId == friendshipId);
|
||||||
|
|
||||||
|
// Rafraîchir la liste des amis
|
||||||
|
final currentUserId = await _getCurrentUserId();
|
||||||
|
if (currentUserId != null) {
|
||||||
|
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');
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rejette une demande d'amitié.
|
||||||
|
Future<void> rejectFriendRequest(String friendshipId) async {
|
||||||
|
try {
|
||||||
|
_logger.i('[LOG] Rejet de la demande d\'amitié: $friendshipId');
|
||||||
|
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');
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annule une demande d'amitié envoyée.
|
||||||
|
Future<void> cancelFriendRequest(String friendshipId) async {
|
||||||
|
try {
|
||||||
|
_logger.i('[LOG] Annulation de la demande d\'amitié: $friendshipId');
|
||||||
|
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');
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:afterwork/domain/entities/friend.dart';
|
import '../../domain/entities/friend.dart';
|
||||||
|
import '../../domain/entities/friend_request.dart';
|
||||||
|
|
||||||
/// Interface [FriendsRepository] définissant les méthodes pour gérer les amis.
|
/// Interface [FriendsRepository] définissant les méthodes pour gérer les amis.
|
||||||
/// Cette interface permet de séparer la logique métier des appels API et de la gestion des données.
|
/// Cette interface permet de séparer la logique métier des appels API et de la gestion des données.
|
||||||
@@ -17,10 +18,11 @@ abstract class FriendsRepository {
|
|||||||
|
|
||||||
/// Envoie une demande pour ajouter un nouvel ami via l'API.
|
/// Envoie une demande pour ajouter un nouvel ami via l'API.
|
||||||
///
|
///
|
||||||
/// [friend] : Objet [Friend] représentant l'ami à ajouter.
|
/// [userId] : Identifiant unique de l'utilisateur qui envoie la demande.
|
||||||
|
/// [friendId] : Identifiant unique de l'ami à ajouter.
|
||||||
///
|
///
|
||||||
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
||||||
Future<void> addFriend(Friend friend);
|
Future<void> addFriend(String userId, String friendId);
|
||||||
|
|
||||||
/// Supprime un ami existant via l'API.
|
/// Supprime un ami existant via l'API.
|
||||||
///
|
///
|
||||||
@@ -45,4 +47,52 @@ abstract class FriendsRepository {
|
|||||||
///
|
///
|
||||||
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
||||||
Future<void> updateFriendStatus(String friendId, String status);
|
Future<void> updateFriendStatus(String friendId, String status);
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié en attente pour un utilisateur.
|
||||||
|
///
|
||||||
|
/// [userId] : Identifiant unique de l'utilisateur.
|
||||||
|
/// [page] : Numéro de la page pour la pagination.
|
||||||
|
/// [size] : Nombre d'éléments par page.
|
||||||
|
///
|
||||||
|
/// Retourne une liste de demandes d'amitié en attente.
|
||||||
|
Future<List<FriendRequest>> getPendingFriendRequests(String userId, int page, int size);
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié envoyées par un utilisateur.
|
||||||
|
///
|
||||||
|
/// [userId] : Identifiant unique de l'utilisateur.
|
||||||
|
/// [page] : Numéro de la page pour la pagination.
|
||||||
|
/// [size] : Nombre d'éléments par page.
|
||||||
|
///
|
||||||
|
/// Retourne une liste de demandes d'amitié envoyées.
|
||||||
|
Future<List<FriendRequest>> getSentFriendRequests(String userId, int page, int size);
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié reçues par un utilisateur.
|
||||||
|
///
|
||||||
|
/// [userId] : Identifiant unique de l'utilisateur.
|
||||||
|
/// [page] : Numéro de la page pour la pagination.
|
||||||
|
/// [size] : Nombre d'éléments par page.
|
||||||
|
///
|
||||||
|
/// Retourne une liste de demandes d'amitié reçues.
|
||||||
|
Future<List<FriendRequest>> getReceivedFriendRequests(String userId, int page, int size);
|
||||||
|
|
||||||
|
/// Accepte une demande d'amitié.
|
||||||
|
///
|
||||||
|
/// [friendshipId] : Identifiant unique de la relation d'amitié.
|
||||||
|
///
|
||||||
|
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
||||||
|
Future<void> acceptFriendRequest(String friendshipId);
|
||||||
|
|
||||||
|
/// Rejette une demande d'amitié.
|
||||||
|
///
|
||||||
|
/// [friendshipId] : Identifiant unique de la relation d'amitié.
|
||||||
|
///
|
||||||
|
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
||||||
|
Future<void> rejectFriendRequest(String friendshipId);
|
||||||
|
|
||||||
|
/// Annule une demande d'amitié envoyée (supprime la relation).
|
||||||
|
///
|
||||||
|
/// [friendshipId] : Identifiant unique de la relation d'amitié.
|
||||||
|
///
|
||||||
|
/// Retourne un `Future<void>`. En cas d'erreur, l'implémentation peut lancer une exception.
|
||||||
|
Future<void> cancelFriendRequest(String friendshipId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,178 +1,685 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logger/logger.dart';
|
|
||||||
|
import '../../core/constants/env_config.dart';
|
||||||
import '../../core/constants/urls.dart';
|
import '../../core/constants/urls.dart';
|
||||||
|
import '../../core/errors/exceptions.dart';
|
||||||
import '../../domain/entities/friend.dart';
|
import '../../domain/entities/friend.dart';
|
||||||
|
import '../../domain/entities/friend_request.dart';
|
||||||
import 'friends_repository.dart';
|
import 'friends_repository.dart';
|
||||||
|
|
||||||
/// Implémentation de [FriendsRepository] pour gérer les appels API relatifs aux amis.
|
/// Implémentation de [FriendsRepository] pour gérer les appels API relatifs aux amis.
|
||||||
/// Chaque action est loguée pour une traçabilité complète et une gestion des erreurs avancée.
|
///
|
||||||
|
/// Cette classe gère toutes les opérations sur les amis via l'API backend,
|
||||||
|
/// avec gestion d'erreurs robuste, timeouts, et validation des réponses.
|
||||||
|
///
|
||||||
|
/// **Usage:**
|
||||||
|
/// ```dart
|
||||||
|
/// final repository = FriendsRepositoryImpl(client: http.Client());
|
||||||
|
/// final friends = await repository.fetchFriends('user123', 0, 20);
|
||||||
|
/// ```
|
||||||
class FriendsRepositoryImpl implements FriendsRepository {
|
class FriendsRepositoryImpl implements FriendsRepository {
|
||||||
final http.Client client;
|
/// Crée une nouvelle instance de [FriendsRepositoryImpl].
|
||||||
final Logger _logger = Logger(); // Logger pour suivre toutes les actions.
|
///
|
||||||
|
/// [client] Le client HTTP à utiliser pour les requêtes
|
||||||
FriendsRepositoryImpl({required this.client});
|
FriendsRepositoryImpl({required this.client});
|
||||||
|
|
||||||
/// Récupère la liste paginée des amis pour un utilisateur donné via l'API.
|
/// Client HTTP pour effectuer les requêtes réseau
|
||||||
///
|
final http.Client client;
|
||||||
/// [userId] : Identifiant unique de l'utilisateur.
|
|
||||||
/// [page] : Page actuelle pour la pagination.
|
|
||||||
/// [size] : Nombre d'amis par page.
|
|
||||||
///
|
|
||||||
/// Retourne une liste d'objets [Friend] ou une liste vide en cas d'erreur.
|
|
||||||
@override
|
|
||||||
Future<List<Friend>> fetchFriends(String userId, int page, int size) async {
|
|
||||||
try {
|
|
||||||
_logger.i("[LOG] Chargement des amis pour l'utilisateur : $userId, page : $page, taille : $size");
|
|
||||||
|
|
||||||
final uri = Uri.parse('${Urls.baseUrl}/friends/list/$userId?page=$page&size=$size');
|
/// Headers par défaut pour les requêtes
|
||||||
_logger.d('[LOG] URL appelée : $uri');
|
static const Map<String, String> _defaultHeaders = {
|
||||||
|
|
||||||
final response = await client.get(uri);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
_logger.i("[LOG] Liste des amis récupérée avec succès.");
|
|
||||||
final List<dynamic> friendsJson = json.decode(response.body);
|
|
||||||
_logger.i("[LOG] Nombre d'amis récupérés (excluant l'utilisateur lui-même) : ${friendsJson.length}");
|
|
||||||
|
|
||||||
return friendsJson.map((json) {
|
|
||||||
_logger.i("[LOG] Conversion JSON -> Friend : $json");
|
|
||||||
final friend = Friend.fromJson(json as Map<String, dynamic>);
|
|
||||||
_logger.i("[LOG] Création d'un objet Friend : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}");
|
|
||||||
return friend;
|
|
||||||
}).toList();
|
|
||||||
} else {
|
|
||||||
_logger.e("[ERROR] Échec de la récupération des amis. Code HTTP : ${response.statusCode}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_logger.e("[ERROR] Exception lors de la récupération des amis : $e");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Envoie une demande pour ajouter un nouvel ami via l'API.
|
|
||||||
///
|
|
||||||
/// [friend] : Objet [Friend] représentant l'ami à ajouter.
|
|
||||||
///
|
|
||||||
/// Loggue chaque étape et lève une exception en cas d'erreur.
|
|
||||||
@override
|
|
||||||
Future<void> addFriend(Friend friend) async {
|
|
||||||
try {
|
|
||||||
_logger.i("[LOG] Tentative d'ajout de l'ami : ${friend.friendFirstName} ${friend.friendLastName}");
|
|
||||||
|
|
||||||
final uri = Uri.parse('${Urls.baseUrl}/friends/send');
|
|
||||||
final response = await client.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: json.encode(friend.toJson()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
_logger.i("[LOG] Ami ajouté avec succès : ${friend.friendId}");
|
|
||||||
} else {
|
|
||||||
_logger.e("[ERROR] Échec lors de l'ajout de l'ami. Code HTTP : ${response.statusCode}");
|
|
||||||
throw Exception("Erreur lors de l'ajout de l'ami");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_logger.e("[ERROR] Exception lors de l'ajout de l'ami : $e");
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supprime un ami existant via l'API.
|
|
||||||
///
|
|
||||||
/// [friendId] : Identifiant unique de l'ami à supprimer.
|
|
||||||
///
|
|
||||||
/// Loggue l'action et lève une exception en cas d'erreur.
|
|
||||||
@override
|
|
||||||
Future<void> removeFriend(String friendId) async {
|
|
||||||
try {
|
|
||||||
_logger.i("[LOG] Tentative de suppression de l'ami avec l'ID : $friendId");
|
|
||||||
|
|
||||||
final uri = Uri.parse('${Urls.baseUrl}/friends/$friendId');
|
|
||||||
final response = await client.delete(uri);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
_logger.i("[LOG] Ami supprimé avec succès : $friendId");
|
|
||||||
} else {
|
|
||||||
_logger.e("[ERROR] Échec lors de la suppression de l'ami. Code HTTP : ${response.statusCode}");
|
|
||||||
throw Exception("Erreur lors de la suppression de l'ami");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_logger.e("[ERROR] Exception lors de la suppression de l'ami : $e");
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Récupère les détails d'un ami en utilisant son identifiant `friendId`.
|
|
||||||
///
|
|
||||||
/// [friendId] : Identifiant unique de l'ami.
|
|
||||||
/// [userId] : Identifiant unique de l'utilisateur connecté.
|
|
||||||
///
|
|
||||||
/// Retourne un `Future<Friend?>` avec les informations de l'ami ou `null` en cas d'échec.
|
|
||||||
@override
|
|
||||||
Future<Friend?> getFriendDetails(String friendId, String userId) async {
|
|
||||||
try {
|
|
||||||
_logger.i("[LOG] Récupération des détails de l'ami avec ID : $friendId pour l'utilisateur : $userId");
|
|
||||||
|
|
||||||
final uri = Uri.parse('${Urls.baseUrl}/friends/details');
|
|
||||||
_logger.d("[LOG] URL pour les détails de l'ami : $uri");
|
|
||||||
|
|
||||||
final headers = {
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': '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 {
|
||||||
|
try {
|
||||||
|
final requestHeaders = {
|
||||||
|
..._defaultHeaders,
|
||||||
|
if (headers != null) ...headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
http.Response response;
|
||||||
|
|
||||||
|
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 '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');
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse une réponse JSON et gère les erreurs.
|
||||||
|
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.
|
||||||
|
void _handleErrorResponse(http.Response response) {
|
||||||
|
String errorMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (response.body.isNotEmpty) {
|
||||||
|
final errorBody = json.decode(response.body);
|
||||||
|
|
||||||
|
// Gérer le format Bean Validation (Quarkus)
|
||||||
|
if (errorBody is Map && errorBody.containsKey('attributeName')) {
|
||||||
|
final attributeName = errorBody['attributeName'] as String? ?? 'champ';
|
||||||
|
final value = errorBody['value'] as String?;
|
||||||
|
final objectName = errorBody['objectName'] as String? ?? '';
|
||||||
|
|
||||||
|
// Construire un message d'erreur plus clair
|
||||||
|
if (attributeName == 'friendId' && value != null) {
|
||||||
|
errorMessage = 'L\'identifiant "$value" n\'est pas valide. Veuillez utiliser l\'ID utilisateur (UUID) et non l\'email.';
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Erreur de validation sur le champ "$attributeName"';
|
||||||
|
if (value != null) {
|
||||||
|
errorMessage += ': valeur "$value" invalide';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Essayer plusieurs formats de réponse d'erreur standard
|
||||||
|
errorMessage = errorBody['message'] as String? ??
|
||||||
|
errorBody['error'] as String? ??
|
||||||
|
errorBody['errorMessage'] as String? ??
|
||||||
|
(errorBody is Map && errorBody.isNotEmpty
|
||||||
|
? errorBody.values.first.toString()
|
||||||
|
: 'Erreur serveur inconnue');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log détaillé pour le débogage
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
_log('Réponse d\'erreur du serveur (${response.statusCode}): ${response.body}');
|
||||||
|
_log('Message d\'erreur extrait: $errorMessage');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Erreur serveur (${response.statusCode})';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Si le parsing JSON échoue, utiliser le body brut
|
||||||
|
errorMessage = response.body.isNotEmpty
|
||||||
|
? response.body
|
||||||
|
: 'Erreur serveur (${response.statusCode})';
|
||||||
|
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
_log('Erreur lors du parsing de la réponse d\'erreur: $e');
|
||||||
|
_log('Body brut: ${response.body}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 400:
|
||||||
|
throw ValidationException(errorMessage);
|
||||||
|
case 401:
|
||||||
|
throw UnauthorizedException(errorMessage);
|
||||||
|
case 404:
|
||||||
|
throw ServerException(
|
||||||
|
'Ressource non trouvée: $errorMessage',
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si une chaîne est un UUID valide
|
||||||
|
bool _isValidUUID(String value) {
|
||||||
|
final uuidRegex = RegExp(
|
||||||
|
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
return uuidRegex.hasMatch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log un message si le mode debug est activé.
|
||||||
|
void _log(String message) {
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
print('[FriendsRepositoryImpl] $message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MÉTHODES PUBLIQUES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Récupère la liste paginée des amis pour un utilisateur donné.
|
||||||
|
///
|
||||||
|
/// [userId] L'identifiant unique de l'utilisateur
|
||||||
|
/// [page] Le numéro de page (commence à 0)
|
||||||
|
/// [size] Le nombre d'amis par page
|
||||||
|
///
|
||||||
|
/// Returns une liste d'objets [Friend]
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final friends = await repository.fetchFriends('user123', 0, 20);
|
||||||
|
/// ```
|
||||||
|
@override
|
||||||
|
Future<List<Friend>> fetchFriends(String userId, int page, int size) async {
|
||||||
|
_log('Récupération des amis pour l\'utilisateur $userId (page $page, taille $size)');
|
||||||
|
|
||||||
|
if (userId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID utilisateur ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page < 0) {
|
||||||
|
throw ValidationException('Le numéro de page doit être >= 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size <= 0) {
|
||||||
|
throw ValidationException('La taille de page doit être > 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse('${Urls.friendsBase}/list/$userId')
|
||||||
|
.replace(queryParameters: {
|
||||||
|
'page': page.toString(),
|
||||||
|
'size': size.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
final response = await _performRequest('GET', uri);
|
||||||
|
|
||||||
|
// Gérer le cas 404 comme une liste vide
|
||||||
|
if (response.statusCode == 404) {
|
||||||
|
_log('Aucun ami trouvé (404) - retour d\'une liste vide');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>?;
|
||||||
|
|
||||||
|
if (jsonResponse == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final friends = jsonResponse
|
||||||
|
.map((json) => Friend.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_log('${friends.length} amis récupérés avec succès');
|
||||||
|
return friends;
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de la récupération des amis: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envoie une demande pour ajouter un nouvel ami.
|
||||||
|
///
|
||||||
|
/// [userId] L'identifiant unique de l'utilisateur qui envoie la demande
|
||||||
|
/// [friendId] L'identifiant unique de l'ami à ajouter
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// await repository.addFriend('user-uuid', 'friend-uuid');
|
||||||
|
/// ```
|
||||||
|
@override
|
||||||
|
Future<void> addFriend(String userId, String friendId) async {
|
||||||
|
_log('Ajout de l\'ami: userId=$userId, friendId=$friendId');
|
||||||
|
|
||||||
|
if (userId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID de l\'utilisateur ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friendId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID de l\'ami ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse('${Urls.friendsBase}/send');
|
||||||
|
|
||||||
|
// Le backend attend userId et friendId dans FriendshipCreateOneRequestDTO
|
||||||
|
final bodyJson = {
|
||||||
|
'userId': userId,
|
||||||
|
'friendId': friendId,
|
||||||
|
};
|
||||||
|
final body = jsonEncode(bodyJson);
|
||||||
|
|
||||||
|
// Log détaillé du body envoyé
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
_log('Envoi de la demande d\'ami à: $uri');
|
||||||
|
_log('Body JSON: $body');
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _performRequest(
|
||||||
|
'POST',
|
||||||
|
uri,
|
||||||
|
body: body,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log de la réponse
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
_log('Réponse du serveur (${response.statusCode}): ${response.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (![200, 201].contains(response.statusCode)) {
|
||||||
|
_handleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Ami ajouté avec succès: $friendId');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de l\'ajout de l\'ami: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprime un ami existant.
|
||||||
|
///
|
||||||
|
/// [friendId] L'identifiant unique de l'ami à supprimer
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// await repository.removeFriend('friend123');
|
||||||
|
/// ```
|
||||||
|
@override
|
||||||
|
Future<void> removeFriend(String friendId) async {
|
||||||
|
_log('Suppression de l\'ami $friendId');
|
||||||
|
|
||||||
|
if (friendId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID de l\'ami ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse('${Urls.friendsBase}/$friendId');
|
||||||
|
final response = await _performRequest('DELETE', uri);
|
||||||
|
|
||||||
|
if (![200, 204].contains(response.statusCode)) {
|
||||||
|
_handleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Ami $friendId supprimé avec succès');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de la suppression de l\'ami $friendId: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les détails d'un ami.
|
||||||
|
///
|
||||||
|
/// [friendId] L'identifiant unique de l'ami
|
||||||
|
/// [userId] L'identifiant unique de l'utilisateur connecté
|
||||||
|
///
|
||||||
|
/// Returns un [Friend] avec les informations de l'ami, ou `null` si non trouvé
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// final friend = await repository.getFriendDetails('friend123', 'user123');
|
||||||
|
/// ```
|
||||||
|
@override
|
||||||
|
Future<Friend?> getFriendDetails(String friendId, String userId) async {
|
||||||
|
_log('Récupération des détails de l\'ami $friendId pour l\'utilisateur $userId');
|
||||||
|
|
||||||
|
if (friendId.isEmpty || userId.isEmpty) {
|
||||||
|
throw ValidationException('Les IDs ne peuvent pas être vides');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse('${Urls.friendsBase}/details');
|
||||||
final body = jsonEncode({
|
final body = jsonEncode({
|
||||||
'friendId': friendId,
|
'friendId': friendId,
|
||||||
'userId': userId,
|
'userId': userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final response = await client.post(uri, headers: headers, body: body);
|
final response = await _performRequest(
|
||||||
_logger.d("[LOG] Réponse de l'API : ${response.body}");
|
'POST',
|
||||||
|
uri,
|
||||||
|
body: body,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
// Gérer le cas 404 comme null
|
||||||
final friendJson = json.decode(response.body);
|
if (response.statusCode == 404) {
|
||||||
_logger.i("[LOG] Détails de l'ami récupérés : $friendJson");
|
_log('Ami $friendId non trouvé (404)');
|
||||||
return Friend.fromJson(friendJson);
|
|
||||||
} else {
|
|
||||||
_logger.e("[ERROR] Échec de la récupération des détails. Code HTTP : ${response.statusCode}");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
_logger.e("[ERROR] Exception lors de la récupération des détails de l'ami : $e");
|
final jsonResponse = _parseJsonResponse(response, [200]) as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (jsonResponse == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final friend = Friend.fromJson(jsonResponse);
|
||||||
|
_log('Détails de l\'ami $friendId récupérés avec succès');
|
||||||
|
return friend;
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de la récupération des détails de l\'ami $friendId: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Met à jour le statut d'un ami (par exemple, "accepté", "bloqué").
|
/// Met à jour le statut d'un ami (par exemple, "accepté", "bloqué").
|
||||||
///
|
///
|
||||||
/// [friendId] : Identifiant unique de l'ami.
|
/// [friendId] L'identifiant unique de l'ami
|
||||||
/// [status] : Nouveau statut sous forme de chaîne de caractères.
|
/// [status] Le nouveau statut sous forme de chaîne de caractères
|
||||||
///
|
///
|
||||||
/// Loggue chaque étape et lève une exception en cas d'échec.
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
///
|
||||||
|
/// **Exemple:**
|
||||||
|
/// ```dart
|
||||||
|
/// await repository.updateFriendStatus('friend123', 'accepted');
|
||||||
|
/// ```
|
||||||
@override
|
@override
|
||||||
Future<void> updateFriendStatus(String friendId, String status) async {
|
Future<void> updateFriendStatus(String friendId, String status) async {
|
||||||
try {
|
_log('Mise à jour du statut de l\'ami $friendId: $status');
|
||||||
_logger.i("[LOG] Mise à jour du statut de l'ami avec l'ID : $friendId, nouveau statut : $status");
|
|
||||||
|
|
||||||
final uri = Uri.parse('${Urls.baseUrl}/friends/$friendId/status');
|
if (friendId.isEmpty) {
|
||||||
final response = await client.patch(
|
throw ValidationException('L\'ID de l\'ami ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isEmpty) {
|
||||||
|
throw ValidationException('Le statut ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse('${Urls.friendsBase}/$friendId/status');
|
||||||
|
final body = jsonEncode({'status': status});
|
||||||
|
|
||||||
|
final response = await _performRequest(
|
||||||
|
'PATCH',
|
||||||
uri,
|
uri,
|
||||||
headers: {'Content-Type': 'application/json'},
|
body: body,
|
||||||
body: jsonEncode({'status': status}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode != 200) {
|
||||||
_logger.i("[LOG] Statut de l'ami mis à jour avec succès : $friendId");
|
_handleErrorResponse(response);
|
||||||
} else {
|
|
||||||
_logger.e("[ERROR] Erreur lors de la mise à jour du statut. Code HTTP : ${response.statusCode}");
|
|
||||||
throw Exception("Erreur lors de la mise à jour du statut");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_log('Statut de l\'ami $friendId mis à jour avec succès');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.e("[ERROR] Exception lors de la mise à jour du statut de l'ami : $e");
|
_log('Erreur lors de la mise à jour du statut de l\'ami $friendId: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié en attente pour un utilisateur.
|
||||||
|
///
|
||||||
|
/// [userId] L'identifiant unique de l'utilisateur
|
||||||
|
/// [page] Le numéro de la page pour la pagination
|
||||||
|
/// [size] La taille de la page
|
||||||
|
///
|
||||||
|
/// Retourne une liste de [FriendRequest]
|
||||||
|
@override
|
||||||
|
Future<List<FriendRequest>> getPendingFriendRequests(String userId, int page, int size) async {
|
||||||
|
_log('Récupération des demandes d\'amitié en attente pour l\'utilisateur $userId (page $page, taille $size)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(Urls.getPendingFriendRequestsWithUserId(userId, page: page, size: size));
|
||||||
|
final response = await _performRequest('GET', uri);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
_handleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>;
|
||||||
|
final requests = jsonResponse
|
||||||
|
.map((json) => FriendRequest.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_log('${requests.length} demandes d\'amitié en attente récupérées avec succès');
|
||||||
|
return requests;
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de la récupération des demandes d\'amitié en attente: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepte une demande d'amitié.
|
||||||
|
///
|
||||||
|
/// [friendshipId] L'identifiant unique de la relation d'amitié
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
@override
|
||||||
|
Future<void> acceptFriendRequest(String friendshipId) async {
|
||||||
|
_log('Acceptation de la demande d\'amitié: $friendshipId');
|
||||||
|
|
||||||
|
if (friendshipId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID de la relation d\'amitié ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(Urls.acceptFriendRequestWithId(friendshipId));
|
||||||
|
final response = await _performRequest('PATCH', uri);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
_handleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Demande d\'amitié acceptée avec succès: $friendshipId');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de l\'acceptation de la demande d\'amitié: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rejette une demande d'amitié.
|
||||||
|
///
|
||||||
|
/// [friendshipId] L'identifiant unique de la relation d'amitié
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
@override
|
||||||
|
Future<void> rejectFriendRequest(String friendshipId) async {
|
||||||
|
_log('Rejet de la demande d\'amitié: $friendshipId');
|
||||||
|
|
||||||
|
if (friendshipId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID de la relation d\'amitié ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(Urls.rejectFriendRequestWithId(friendshipId));
|
||||||
|
final response = await _performRequest('PATCH', uri);
|
||||||
|
|
||||||
|
if (response.statusCode != 204) {
|
||||||
|
_handleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Demande d\'amitié rejetée avec succès: $friendshipId');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors du rejet de la demande d\'amitié: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié envoyées par un utilisateur.
|
||||||
|
///
|
||||||
|
/// [userId] L'identifiant unique de l'utilisateur
|
||||||
|
/// [page] Le numéro de page (commence à 0)
|
||||||
|
/// [size] Le nombre de demandes par page
|
||||||
|
///
|
||||||
|
/// Returns une liste d'objets [FriendRequest]
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
@override
|
||||||
|
Future<List<FriendRequest>> getSentFriendRequests(String userId, int page, int size) async {
|
||||||
|
_log('Récupération des demandes d\'amitié envoyées pour l\'utilisateur $userId (page $page, taille $size)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(Urls.getSentFriendRequestsWithUserId(userId, page: page, size: size));
|
||||||
|
final response = await _performRequest('GET', uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 404) {
|
||||||
|
_log('Aucune demande envoyée trouvée (404) - retour d\'une liste vide');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>;
|
||||||
|
final requests = jsonResponse
|
||||||
|
.map((json) => FriendRequest.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_log('${requests.length} demandes d\'amitié envoyées récupérées avec succès');
|
||||||
|
return requests;
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de la récupération des demandes d\'amitié envoyées: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les demandes d'amitié reçues par un utilisateur.
|
||||||
|
///
|
||||||
|
/// [userId] L'identifiant unique de l'utilisateur
|
||||||
|
/// [page] Le numéro de page (commence à 0)
|
||||||
|
/// [size] Le nombre de demandes par page
|
||||||
|
///
|
||||||
|
/// Returns une liste d'objets [FriendRequest]
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
@override
|
||||||
|
Future<List<FriendRequest>> getReceivedFriendRequests(String userId, int page, int size) async {
|
||||||
|
_log('Récupération des demandes d\'amitié reçues pour l\'utilisateur $userId (page $page, taille $size)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(Urls.getReceivedFriendRequestsWithUserId(userId, page: page, size: size));
|
||||||
|
final response = await _performRequest('GET', uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 404) {
|
||||||
|
_log('Aucune demande reçue trouvée (404) - retour d\'une liste vide');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final jsonResponse = _parseJsonResponse(response, [200]) as List<dynamic>;
|
||||||
|
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
_log('Réponse JSON brute: $jsonResponse');
|
||||||
|
}
|
||||||
|
|
||||||
|
final requests = jsonResponse
|
||||||
|
.map((json) {
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
_log('Parsing demande: $json');
|
||||||
|
}
|
||||||
|
return FriendRequest.fromJson(json as Map<String, dynamic>);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_log('${requests.length} demandes d\'amitié reçues récupérées avec succès');
|
||||||
|
if (EnvConfig.enableDetailedLogs && requests.isNotEmpty) {
|
||||||
|
_log('Première demande: userId=${requests.first.userId}, friendId=${requests.first.friendId}, userFullName=${requests.first.userFullName}');
|
||||||
|
}
|
||||||
|
return requests;
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de la récupération des demandes d\'amitié reçues: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annule une demande d'amitié envoyée (supprime la relation).
|
||||||
|
///
|
||||||
|
/// [friendshipId] L'identifiant unique de la relation d'amitié
|
||||||
|
///
|
||||||
|
/// Throws [ServerException] en cas d'erreur
|
||||||
|
@override
|
||||||
|
Future<void> cancelFriendRequest(String friendshipId) async {
|
||||||
|
_log('Annulation de la demande d\'amitié: $friendshipId');
|
||||||
|
|
||||||
|
if (friendshipId.isEmpty) {
|
||||||
|
throw ValidationException('L\'ID de la relation d\'amitié ne peut pas être vide');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Utiliser l'endpoint DELETE existant pour supprimer la relation
|
||||||
|
final uri = Uri.parse('${Urls.friendsBase}/$friendshipId');
|
||||||
|
final response = await _performRequest('DELETE', uri);
|
||||||
|
|
||||||
|
if (response.statusCode != 204) {
|
||||||
|
_handleErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log('Demande d\'amitié annulée avec succès: $friendshipId');
|
||||||
|
} catch (e) {
|
||||||
|
_log('Erreur lors de l\'annulation de la demande d\'amitié: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
lib/domain/entities/friend_request.dart
Normal file
83
lib/domain/entities/friend_request.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Entité représentant une demande d'amitié.
|
||||||
|
/// Contient les informations nécessaires pour gérer les demandes d'amitié en attente.
|
||||||
|
class FriendRequest extends Equatable {
|
||||||
|
const FriendRequest({
|
||||||
|
required this.friendshipId,
|
||||||
|
required this.userId,
|
||||||
|
required this.userFirstName,
|
||||||
|
required this.userLastName,
|
||||||
|
required this.friendId,
|
||||||
|
required this.friendFirstName,
|
||||||
|
required this.friendLastName,
|
||||||
|
required this.status,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// ID de la relation d'amitié
|
||||||
|
final String friendshipId;
|
||||||
|
|
||||||
|
/// ID de l'utilisateur qui a envoyé la demande
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
/// Prénom de l'utilisateur qui a envoyé la demande
|
||||||
|
final String userFirstName;
|
||||||
|
|
||||||
|
/// Nom de l'utilisateur qui a envoyé la demande
|
||||||
|
final String userLastName;
|
||||||
|
|
||||||
|
/// ID de l'utilisateur qui a reçu la demande
|
||||||
|
final String friendId;
|
||||||
|
|
||||||
|
/// Prénom de l'utilisateur qui a reçu la demande
|
||||||
|
final String friendFirstName;
|
||||||
|
|
||||||
|
/// Nom de l'utilisateur qui a reçu la demande
|
||||||
|
final String friendLastName;
|
||||||
|
|
||||||
|
/// Statut de la demande (PENDING, ACCEPTED, REJECTED)
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
/// Date de création de la demande
|
||||||
|
final String createdAt;
|
||||||
|
|
||||||
|
/// Nom complet de l'utilisateur qui a envoyé la demande
|
||||||
|
String get userFullName => '$userFirstName $userLastName'.trim();
|
||||||
|
|
||||||
|
/// Nom complet de l'utilisateur qui a reçu la demande
|
||||||
|
String get friendFullName => '$friendFirstName $friendLastName'.trim();
|
||||||
|
|
||||||
|
/// Factory constructor pour créer un [FriendRequest] à partir d'un JSON
|
||||||
|
factory FriendRequest.fromJson(Map<String, dynamic> json) {
|
||||||
|
// Le backend envoie : userNom (nom de famille), userPrenoms (prénoms)
|
||||||
|
// friendNom (nom de famille), friendPrenoms (prénoms)
|
||||||
|
return FriendRequest(
|
||||||
|
friendshipId: json['friendshipId']?.toString() ?? json['id']?.toString() ?? '',
|
||||||
|
userId: json['userId']?.toString() ?? '',
|
||||||
|
// userNom = nom de famille (lastName), userPrenoms = prénoms (firstName)
|
||||||
|
userFirstName: json['userPrenoms']?.toString() ?? json['userFirstName']?.toString() ?? '',
|
||||||
|
userLastName: json['userNom']?.toString() ?? json['userLastName']?.toString() ?? '',
|
||||||
|
friendId: json['friendId']?.toString() ?? '',
|
||||||
|
// friendNom = nom de famille (lastName), friendPrenoms = prénoms (firstName)
|
||||||
|
friendFirstName: json['friendPrenoms']?.toString() ?? json['friendFirstName']?.toString() ?? '',
|
||||||
|
friendLastName: json['friendNom']?.toString() ?? json['friendLastName']?.toString() ?? '',
|
||||||
|
status: json['status']?.toString() ?? 'PENDING',
|
||||||
|
createdAt: json['createdAt']?.toString() ?? json['created_at']?.toString() ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
friendshipId,
|
||||||
|
userId,
|
||||||
|
userFirstName,
|
||||||
|
userLastName,
|
||||||
|
friendId,
|
||||||
|
friendFirstName,
|
||||||
|
friendLastName,
|
||||||
|
status,
|
||||||
|
createdAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,176 +1,247 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../../data/providers/friends_provider.dart';
|
import '../../../data/providers/friends_provider.dart';
|
||||||
import '../../../domain/entities/friend.dart';
|
import '../../../domain/entities/friend_request.dart';
|
||||||
import '../../widgets/friend_detail_screen.dart';
|
import '../../widgets/add_friend_dialog.dart';
|
||||||
|
import '../../widgets/cards/friend_card.dart';
|
||||||
|
import '../../widgets/friend_request_card.dart';
|
||||||
import '../../widgets/search_friends.dart';
|
import '../../widgets/search_friends.dart';
|
||||||
|
|
||||||
/// [FriendsScreen] est l'écran principal permettant d'afficher et de gérer la liste des amis.
|
/// Écran principal pour afficher et gérer la liste des amis.
|
||||||
/// Il inclut des fonctionnalités de pagination, de recherche, et de rafraîchissement manuel de la liste.
|
///
|
||||||
/// Ce widget est un [StatefulWidget] afin de pouvoir mettre à jour dynamiquement la liste des amis.
|
/// Cet écran inclut des fonctionnalités de pagination, de recherche,
|
||||||
|
/// et de rafraîchissement manuel de la liste avec design moderne et compact.
|
||||||
|
///
|
||||||
|
/// **Fonctionnalités:**
|
||||||
|
/// - Affichage de la liste des amis en grille
|
||||||
|
/// - Recherche d'amis
|
||||||
|
/// - Pagination automatique
|
||||||
|
/// - Pull-to-refresh
|
||||||
|
/// - Ajout d'amis
|
||||||
class FriendsScreen extends StatefulWidget {
|
class FriendsScreen extends StatefulWidget {
|
||||||
final String userId; // Identifiant de l'utilisateur pour récupérer ses amis
|
const FriendsScreen({required this.userId, super.key});
|
||||||
|
|
||||||
const FriendsScreen({Key? key, required this.userId}) : super(key: key);
|
final String userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FriendsScreenState createState() => _FriendsScreenState();
|
State<FriendsScreen> createState() => _FriendsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FriendsScreenState extends State<FriendsScreen> {
|
class _FriendsScreenState extends State<FriendsScreen> with SingleTickerProviderStateMixin {
|
||||||
|
// ============================================================================
|
||||||
|
// CONTROLLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
late ScrollController _scrollController;
|
late ScrollController _scrollController;
|
||||||
|
late TabController _tabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Initialisation du contrôleur de défilement pour la gestion de la pagination.
|
_initializeScrollController();
|
||||||
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
|
_loadFriends();
|
||||||
|
_loadSentRequests();
|
||||||
|
_loadReceivedRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeScrollController() {
|
||||||
_scrollController = ScrollController();
|
_scrollController = ScrollController();
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
|
|
||||||
// Log pour indiquer le début du chargement des amis
|
|
||||||
debugPrint("[LOG] Initialisation de la page : chargement des amis pour l'utilisateur ${widget.userId}");
|
|
||||||
// Chargement initial de la liste d'amis via le fournisseur (Provider)
|
|
||||||
Provider.of<FriendsProvider>(context, listen: false).fetchFriends(widget.userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Nettoyage du contrôleur de défilement pour éviter les fuites de mémoire.
|
|
||||||
_scrollController.removeListener(_onScroll);
|
_scrollController.removeListener(_onScroll);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("[LOG] Dispose : contrôleur de défilement supprimé");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Méthode déclenchée lors du défilement de la liste.
|
// ============================================================================
|
||||||
/// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis.
|
// ACTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Charge les amis au démarrage.
|
||||||
|
void _loadFriends() {
|
||||||
|
Provider.of<FriendsProvider>(context, listen: false)
|
||||||
|
.fetchFriends(widget.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charge les demandes envoyées au démarrage.
|
||||||
|
void _loadSentRequests() {
|
||||||
|
Provider.of<FriendsProvider>(context, listen: false)
|
||||||
|
.fetchSentRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charge les demandes reçues au démarrage.
|
||||||
|
void _loadReceivedRequests() {
|
||||||
|
Provider.of<FriendsProvider>(context, listen: false)
|
||||||
|
.fetchReceivedRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gère le défilement pour la pagination.
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
final provider = Provider.of<FriendsProvider>(context, listen: false);
|
final provider = Provider.of<FriendsProvider>(context, listen: false);
|
||||||
|
|
||||||
// Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt.
|
|
||||||
if (_scrollController.position.pixels >=
|
if (_scrollController.position.pixels >=
|
||||||
_scrollController.position.maxScrollExtent - 200 &&
|
_scrollController.position.maxScrollExtent - 200 &&
|
||||||
!provider.isLoading && provider.hasMore) {
|
!provider.isLoading &&
|
||||||
debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante.");
|
provider.hasMore) {
|
||||||
provider.fetchFriends(widget.userId, loadMore: true); // Chargement de plus d'amis
|
provider.fetchFriends(widget.userId, loadMore: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gère le rafraîchissement.
|
||||||
|
Future<void> _handleRefresh() async {
|
||||||
|
final provider = Provider.of<FriendsProvider>(context, listen: false);
|
||||||
|
provider.fetchFriends(widget.userId);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gère l'ajout d'un ami.
|
||||||
|
void _handleAddFriend() {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AddFriendDialog(
|
||||||
|
onFriendAdded: () {
|
||||||
|
// Rafraîchir la liste des amis et des demandes après l'ajout
|
||||||
|
_loadFriends();
|
||||||
|
_loadSentRequests();
|
||||||
|
_loadReceivedRequests();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// BUILD
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Accès au fournisseur pour gérer les données et les états des amis.
|
final theme = Theme.of(context);
|
||||||
final friendsProvider = Provider.of<FriendsProvider>(context, listen: false);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: _buildAppBar(theme),
|
||||||
title: const Text('Mes Amis'),
|
body: _buildBody(theme),
|
||||||
actions: [
|
floatingActionButton: _buildFloatingActionButton(theme),
|
||||||
// Bouton pour rafraîchir la liste des amis
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
onPressed: () {
|
|
||||||
// Vérifie si la liste n'est pas en cours de chargement avant d'envoyer une nouvelle requête.
|
|
||||||
if (!friendsProvider.isLoading) {
|
|
||||||
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
|
|
||||||
friendsProvider.fetchFriends(widget.userId);
|
|
||||||
} else {
|
|
||||||
debugPrint("[LOG] Rafraîchissement en cours, action ignorée.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Widget de recherche d'amis en haut de l'écran
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: SearchFriends(),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
// Construction de la liste d'amis avec un affichage en grille
|
|
||||||
child: Consumer<FriendsProvider>(
|
|
||||||
builder: (context, friendsProvider, child) {
|
|
||||||
// Si le chargement est en cours et qu'il n'y a aucun ami, afficher un indicateur de chargement.
|
|
||||||
if (friendsProvider.isLoading && friendsProvider.friendsList.isEmpty) {
|
|
||||||
debugPrint("[LOG] Chargement : affichage de l'indicateur de progression");
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si la liste est vide après le chargement, afficher un message indiquant qu'aucun ami n'a été trouvé.
|
|
||||||
if (friendsProvider.friendsList.isEmpty) {
|
|
||||||
debugPrint("[LOG] Liste vide : Aucun ami trouvé");
|
|
||||||
return const Center(
|
|
||||||
child: Text('Aucun ami trouvé'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return GridView.builder(
|
/// Construit la barre d'application.
|
||||||
controller: _scrollController, // Utilisation du contrôleur pour la pagination
|
PreferredSizeWidget _buildAppBar(ThemeData theme) {
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
return AppBar(
|
||||||
crossAxisCount: 2, // Deux amis par ligne
|
title: const Text('Mes Amis'),
|
||||||
crossAxisSpacing: 10,
|
bottom: TabBar(
|
||||||
mainAxisSpacing: 10,
|
controller: _tabController,
|
||||||
childAspectRatio: 0.8, // Ajuste la taille des cartes
|
tabs: const [
|
||||||
|
Tab(text: 'Amis', icon: Icon(Icons.people)),
|
||||||
|
Tab(text: 'Demandes', icon: Icon(Icons.person_add)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
itemCount: friendsProvider.friendsList.length,
|
actions: [
|
||||||
itemBuilder: (context, index) {
|
IconButton(
|
||||||
final friend = friendsProvider.friendsList[index];
|
icon: const Icon(Icons.refresh),
|
||||||
// Affichage de chaque ami dans une carte avec une animation
|
tooltip: 'Actualiser',
|
||||||
return GestureDetector(
|
onPressed: () {
|
||||||
onTap: () => _navigateToFriendDetail(context, friend), // Action au clic sur l'avatar
|
_loadFriends();
|
||||||
child: AnimatedContainer(
|
_loadSentRequests();
|
||||||
duration: const Duration(milliseconds: 300),
|
_loadReceivedRequests();
|
||||||
curve: Curves.easeInOut,
|
},
|
||||||
transform: Matrix4.identity()
|
|
||||||
..scale(1.05), // Effet de zoom lors du survol
|
|
||||||
child: Card(
|
|
||||||
elevation: 6,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le corps de l'écran.
|
||||||
|
Widget _buildBody(ThemeData theme) {
|
||||||
|
return SafeArea(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
// Onglet Amis
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_buildSearchBar(),
|
||||||
|
Expanded(
|
||||||
|
child: Consumer<FriendsProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
if (provider.isLoading && provider.friendsList.isEmpty) {
|
||||||
|
return _buildLoadingState(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.friendsList.isEmpty) {
|
||||||
|
return _buildEmptyState(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildFriendsList(theme, provider);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Onglet Demandes en attente
|
||||||
|
_buildPendingRequestsTab(theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit la barre de recherche.
|
||||||
|
Widget _buildSearchBar() {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: SearchFriends(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit l'état de chargement.
|
||||||
|
Widget _buildLoadingState(ThemeData theme) {
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircularProgressIndicator(
|
||||||
radius: 50,
|
color: theme.colorScheme.primary,
|
||||||
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
|
|
||||||
? (friend.imageUrl!.startsWith('https') // Vérifie si l'image est une URL réseau.
|
|
||||||
? NetworkImage(friend.imageUrl!) // Charge l'image depuis une URL réseau.
|
|
||||||
: AssetImage(friend.imageUrl!) as ImageProvider) // Sinon, charge depuis les ressources locales.
|
|
||||||
: const AssetImage('lib/assets/images/default_avatar.png'), // Si aucune image, utilise l'image par défaut.
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
"${friend.friendFirstName} ${friend.friendLastName}",
|
'Chargement des amis...',
|
||||||
style: const TextStyle(
|
style: theme.textTheme.bodyMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 5),
|
|
||||||
Text(
|
|
||||||
friend.status.name,
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 5),
|
|
||||||
Text(
|
|
||||||
friend.lastInteraction ?? 'Aucune interaction récente',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
},
|
/// Construit l'état vide.
|
||||||
|
Widget _buildEmptyState(ThemeData theme) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.people_outline,
|
||||||
|
size: 64,
|
||||||
|
color: theme.colorScheme.secondary.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Aucun ami trouvé',
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Commencez à ajouter des amis pour voir leurs événements',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -178,23 +249,226 @@ class _FriendsScreenState extends State<FriendsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigation vers l'écran de détails de l'ami
|
/// Construit la liste des amis.
|
||||||
/// Permet de voir les informations complètes d'un ami lorsque l'utilisateur clique sur son avatar.
|
Widget _buildFriendsList(ThemeData theme, FriendsProvider provider) {
|
||||||
void _navigateToFriendDetail(BuildContext context, Friend friend) {
|
return RefreshIndicator(
|
||||||
debugPrint("[LOG] Navigation : Détails de l'ami ${friend.friendFirstName} ${friend.friendLastName}");
|
onRefresh: _handleRefresh,
|
||||||
Navigator.push(
|
color: theme.colorScheme.primary,
|
||||||
context,
|
child: GridView.builder(
|
||||||
MaterialPageRoute(
|
controller: _scrollController,
|
||||||
builder: (context) => FriendDetailScreen(
|
padding: const EdgeInsets.all(16),
|
||||||
friendFirstName: friend.friendFirstName, // Prénom de l'ami
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
friendLastName: friend.friendLastName, // Nom de l'ami
|
crossAxisCount: 2,
|
||||||
imageUrl: friend.imageUrl ?? '', // URL de l'image de l'ami (ou valeur par défaut)
|
crossAxisSpacing: 16,
|
||||||
friendId: friend.friendId, // Identifiant unique de l'ami
|
mainAxisSpacing: 16,
|
||||||
status: friend.status, // Statut de l'ami
|
childAspectRatio: 0.75,
|
||||||
lastInteraction: friend.lastInteraction ?? 'Aucune', // Dernière interaction (si disponible)
|
|
||||||
dateAdded: friend.dateAdded ?? 'Inconnu', // Date d'ajout de l'ami (si disponible)
|
|
||||||
),
|
),
|
||||||
|
itemCount: provider.friendsList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final friend = provider.friendsList[index];
|
||||||
|
return FriendCard(friend: friend);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le bouton flottant (compact).
|
||||||
|
Widget _buildFloatingActionButton(ThemeData theme) {
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: _handleAddFriend,
|
||||||
|
tooltip: 'Ajouter un ami',
|
||||||
|
child: const Icon(Icons.person_add),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit l'onglet des demandes en attente avec deux sections.
|
||||||
|
Widget _buildPendingRequestsTab(ThemeData theme) {
|
||||||
|
return Consumer<FriendsProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
final isLoading = provider.isLoadingReceivedRequests || provider.isLoadingSentRequests;
|
||||||
|
final hasReceived = provider.receivedRequests.isNotEmpty;
|
||||||
|
final hasSent = provider.sentRequests.isNotEmpty;
|
||||||
|
|
||||||
|
if (isLoading && !hasReceived && !hasSent) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Chargement des demandes...',
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasReceived && !hasSent) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.person_add_disabled,
|
||||||
|
size: 64,
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Aucune demande en attente',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Les demandes d\'amitié apparaîtront ici',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await Future.wait([
|
||||||
|
provider.fetchReceivedRequests(),
|
||||||
|
provider.fetchSentRequests(),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
children: [
|
||||||
|
// Section Demandes reçues
|
||||||
|
if (hasReceived) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
'Demandes reçues',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...provider.receivedRequests.map((request) => FriendRequestCard(
|
||||||
|
request: request,
|
||||||
|
onAccept: () => _handleAcceptRequest(provider, request.friendshipId),
|
||||||
|
onReject: () => _handleRejectRequest(provider, request.friendshipId),
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
// Section Demandes envoyées
|
||||||
|
if (hasSent) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
'Demandes envoyées',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...provider.sentRequests.map((request) => FriendRequestCard(
|
||||||
|
request: request,
|
||||||
|
onAccept: null, // Pas d'accepter pour les demandes envoyées
|
||||||
|
onReject: () => _handleCancelRequest(provider, request.friendshipId),
|
||||||
|
isSentRequest: true, // Indique que c'est une demande envoyée
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gère l'acceptation d'une demande d'amitié.
|
||||||
|
Future<void> _handleAcceptRequest(FriendsProvider provider, String friendshipId) async {
|
||||||
|
try {
|
||||||
|
await provider.acceptFriendRequest(friendshipId);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Demande d\'amitié acceptée'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Rafraîchir les deux onglets
|
||||||
|
_loadFriends();
|
||||||
|
_loadReceivedRequests();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gère le rejet d'une demande d'amitié.
|
||||||
|
Future<void> _handleRejectRequest(FriendsProvider provider, String friendshipId) async {
|
||||||
|
try {
|
||||||
|
await provider.rejectFriendRequest(friendshipId);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Demande d\'amitié rejetée'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_loadReceivedRequests();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gère l'annulation d'une demande d'amitié envoyée.
|
||||||
|
Future<void> _handleCancelRequest(FriendsProvider provider, String friendshipId) async {
|
||||||
|
try {
|
||||||
|
await provider.cancelFriendRequest(friendshipId);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Demande d\'amitié annulée'),
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_loadSentRequests();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
448
lib/presentation/widgets/add_friend_dialog.dart
Normal file
448
lib/presentation/widgets/add_friend_dialog.dart
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../core/constants/env_config.dart';
|
||||||
|
import '../../core/constants/urls.dart';
|
||||||
|
import '../../data/datasources/user_remote_data_source.dart';
|
||||||
|
import '../../data/models/user_model.dart';
|
||||||
|
import '../../data/providers/friends_provider.dart';
|
||||||
|
import '../../data/services/secure_storage.dart';
|
||||||
|
import '../../domain/entities/friend.dart';
|
||||||
|
|
||||||
|
/// Dialogue pour rechercher et ajouter un nouvel ami.
|
||||||
|
///
|
||||||
|
/// Ce widget permet de rechercher des utilisateurs par email ou nom,
|
||||||
|
/// puis d'envoyer une demande d'ami.
|
||||||
|
class AddFriendDialog extends StatefulWidget {
|
||||||
|
const AddFriendDialog({
|
||||||
|
required this.onFriendAdded,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Callback appelé lorsqu'un ami est ajouté avec succès
|
||||||
|
final VoidCallback onFriendAdded;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddFriendDialog> createState() => _AddFriendDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddFriendDialogState extends State<AddFriendDialog> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final SecureStorage _secureStorage = SecureStorage();
|
||||||
|
late final UserRemoteDataSource _userDataSource;
|
||||||
|
|
||||||
|
List<UserModel> _searchResults = [];
|
||||||
|
bool _isSearching = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
String? _currentUserId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_userDataSource = UserRemoteDataSource(http.Client());
|
||||||
|
_loadCurrentUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charge l'ID de l'utilisateur actuel
|
||||||
|
Future<void> _loadCurrentUserId() async {
|
||||||
|
final userId = await _secureStorage.getUserId();
|
||||||
|
setState(() {
|
||||||
|
_currentUserId = userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recherche des utilisateurs par email
|
||||||
|
Future<void> _searchUsers(String query) async {
|
||||||
|
if (query.trim().isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final email = query.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!_isValidEmail(email)) {
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_errorMessage = 'Veuillez entrer un email valide';
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSearching = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
_searchResults = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Rechercher l'utilisateur par email via l'API
|
||||||
|
final user = await _userDataSource.searchUserByEmail(email);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [user];
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
debugPrint('[AddFriendDialog] Erreur de recherche: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
String errorMessage;
|
||||||
|
if (e.toString().contains('UserNotFoundException') || e.toString().contains('non trouvé')) {
|
||||||
|
errorMessage = 'Aucun utilisateur trouvé avec cet email';
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Erreur lors de la recherche: ${e.toString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = errorMessage;
|
||||||
|
_isSearching = false;
|
||||||
|
_searchResults = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si l'email est valide
|
||||||
|
bool _isValidEmail(String email) {
|
||||||
|
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajoute un ami par son UUID
|
||||||
|
Future<void> _addFriend(String friendId) async {
|
||||||
|
if (_currentUserId == null || _currentUserId!.isEmpty) {
|
||||||
|
_showError('Vous devez être connecté pour ajouter un ami');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friendId.trim().isEmpty) {
|
||||||
|
_showError('ID utilisateur invalide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? friendEmail;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_isSearching = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les informations de l'utilisateur par son ID pour affichage
|
||||||
|
try {
|
||||||
|
final user = await _userDataSource.getUser(friendId);
|
||||||
|
friendEmail = user.email;
|
||||||
|
} catch (e) {
|
||||||
|
// Si l'utilisateur n'est pas trouvé, on continue quand même avec l'ID
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
debugPrint('[AddFriendDialog] Impossible de récupérer les infos utilisateur: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoyer la demande d'ami via le provider (seulement l'UUID est nécessaire)
|
||||||
|
final friendsProvider = Provider.of<FriendsProvider>(context, listen: false);
|
||||||
|
await friendsProvider.addFriend(friendId);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Demande d\'ami envoyée${friendEmail != null ? ' à $friendEmail' : ''}'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
widget.onFriendAdded();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
debugPrint('[AddFriendDialog] Erreur lors de l\'ajout: $e');
|
||||||
|
debugPrint('[AddFriendDialog] Type d\'erreur: ${e.runtimeType}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraire un message d'erreur plus clair
|
||||||
|
String errorMessage;
|
||||||
|
if (e.toString().contains('ValidationException')) {
|
||||||
|
// Extraire le message après "ValidationException: "
|
||||||
|
final parts = e.toString().split('ValidationException: ');
|
||||||
|
errorMessage = parts.length > 1 ? parts[1] : 'Données invalides';
|
||||||
|
} else if (e.toString().contains('ServerException')) {
|
||||||
|
final parts = e.toString().split('ServerException: ');
|
||||||
|
errorMessage = parts.length > 1 ? parts[1] : 'Erreur serveur';
|
||||||
|
} else if (e.toString().contains('ConflictException')) {
|
||||||
|
final parts = e.toString().split('ConflictException: ');
|
||||||
|
errorMessage = parts.length > 1 ? parts[1] : 'Cet utilisateur est déjà votre ami';
|
||||||
|
} else {
|
||||||
|
errorMessage = e.toString().replaceAll(RegExp(r'^[A-Za-z]+Exception: '), '');
|
||||||
|
if (errorMessage.isEmpty || errorMessage == e.toString()) {
|
||||||
|
errorMessage = 'Erreur lors de l\'ajout de l\'ami. Veuillez réessayer.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_showError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Affiche une erreur
|
||||||
|
void _showError(String message) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildHeader(theme),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildSearchField(theme),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_errorMessage != null) _buildErrorMessage(theme),
|
||||||
|
if (_isSearching) _buildLoadingIndicator(theme),
|
||||||
|
if (!_isSearching && _searchResults.isNotEmpty)
|
||||||
|
_buildSearchResults(theme),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildActions(theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit l'en-tête du dialogue
|
||||||
|
Widget _buildHeader(ThemeData theme) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.person_add,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Ajouter un ami',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le champ de recherche
|
||||||
|
Widget _buildSearchField(ThemeData theme) {
|
||||||
|
return TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Entrez l\'email de l\'ami',
|
||||||
|
prefixIcon: const Icon(Icons.person_search),
|
||||||
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
helperText: 'Recherchez un utilisateur par son adresse email',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
// Recherche automatique après un délai
|
||||||
|
if (value.trim().isNotEmpty && _isValidEmail(value.trim())) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (_searchController.text == value) {
|
||||||
|
_searchUsers(value.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
onSubmitted: (value) {
|
||||||
|
if (value.trim().isNotEmpty) {
|
||||||
|
_searchUsers(value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le message d'erreur
|
||||||
|
Widget _buildErrorMessage(ThemeData theme) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit l'indicateur de chargement
|
||||||
|
Widget _buildLoadingIndicator(ThemeData theme) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit les résultats de recherche
|
||||||
|
Widget _buildSearchResults(ThemeData theme) {
|
||||||
|
if (_isSearching) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_searchResults.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: _searchResults.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = _searchResults[index];
|
||||||
|
return _buildUserTile(theme, user);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit une tuile d'utilisateur
|
||||||
|
Widget _buildUserTile(ThemeData theme, UserModel user) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
backgroundImage: user.profileImageUrl.isNotEmpty &&
|
||||||
|
user.profileImageUrl.startsWith('http')
|
||||||
|
? NetworkImage(user.profileImageUrl)
|
||||||
|
: null,
|
||||||
|
onBackgroundImageError: (exception, stackTrace) {
|
||||||
|
// Ignorer les erreurs de chargement d'image
|
||||||
|
if (EnvConfig.enableDetailedLogs) {
|
||||||
|
debugPrint('[AddFriendDialog] Erreur de chargement d\'image: $exception');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: user.profileImageUrl.isEmpty ||
|
||||||
|
!user.profileImageUrl.startsWith('http')
|
||||||
|
? Text(
|
||||||
|
user.userFirstName.isNotEmpty
|
||||||
|
? user.userFirstName[0].toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'${user.userFirstName} ${user.userLastName}'.trim(),
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
user.email,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.person_add),
|
||||||
|
onPressed: _isSearching ? null : () => _addFriend(user.userId),
|
||||||
|
tooltip: 'Ajouter comme ami',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit les actions du dialogue
|
||||||
|
Widget _buildActions(ThemeData theme) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,11 @@ import '../../domain/entities/friend.dart';
|
|||||||
/// et une option pour envoyer un message.
|
/// et une option pour envoyer un message.
|
||||||
/// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails.
|
/// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails.
|
||||||
class FriendDetailScreen extends StatelessWidget {
|
class FriendDetailScreen extends StatelessWidget {
|
||||||
|
|
||||||
|
/// Constructeur de la classe [FriendDetailScreen].
|
||||||
|
FriendDetailScreen({
|
||||||
|
required this.friendFirstName, required this.friendLastName, required this.imageUrl, required this.friendId, required this.status, required this.lastInteraction, required this.dateAdded, super.key,
|
||||||
|
});
|
||||||
final String friendFirstName; // Nom de l'ami
|
final String friendFirstName; // Nom de l'ami
|
||||||
final String friendLastName;
|
final String friendLastName;
|
||||||
final String imageUrl; // URL de l'image de profil de l'ami
|
final String imageUrl; // URL de l'image de profil de l'ami
|
||||||
@@ -16,18 +21,6 @@ class FriendDetailScreen extends StatelessWidget {
|
|||||||
final String lastInteraction;
|
final String lastInteraction;
|
||||||
final String dateAdded;
|
final String dateAdded;
|
||||||
|
|
||||||
/// Constructeur de la classe [FriendDetailScreen].
|
|
||||||
FriendDetailScreen({
|
|
||||||
Key? key,
|
|
||||||
required this.friendFirstName,
|
|
||||||
required this.friendLastName,
|
|
||||||
required this.imageUrl,
|
|
||||||
required this.friendId,
|
|
||||||
required this.status,
|
|
||||||
required this.lastInteraction,
|
|
||||||
required this.dateAdded,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
/// Méthode statique pour lancer l'écran des détails d'un ami.
|
/// Méthode statique pour lancer l'écran des détails d'un ami.
|
||||||
static void open(
|
static void open(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -37,7 +30,7 @@ class FriendDetailScreen extends StatelessWidget {
|
|||||||
String imageUrl,
|
String imageUrl,
|
||||||
FriendStatus status,
|
FriendStatus status,
|
||||||
String lastInteraction,
|
String lastInteraction,
|
||||||
String dateAdded) {
|
String dateAdded,) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -54,13 +47,41 @@ class FriendDetailScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Vérifie si une URL est valide pour le chargement d'image réseau.
|
||||||
|
bool _isValidImageUrl(String? url) {
|
||||||
|
if (url == null || url.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.tryParse(url);
|
||||||
|
if (uri == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que c'est une URL HTTP/HTTPS valide
|
||||||
|
if (!uri.hasScheme || (!uri.scheme.startsWith('http'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier qu'il y a un host
|
||||||
|
if (uri.host.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.i('[LOG] Affichage des détails de l\'ami : $friendFirstName (ID: $friendId)');
|
_logger.i('[LOG] Affichage des détails de l\'ami : $friendFirstName (ID: $friendId)');
|
||||||
|
|
||||||
// Utilise `AssetImage` si `imageUrl` est vide ou ne contient pas d'URL valide.
|
// Vérifier si l'URL est valide avant de créer le NetworkImage
|
||||||
final imageProvider =
|
final bool hasValidImageUrl = _isValidImageUrl(imageUrl);
|
||||||
imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true
|
final ImageProvider imageProvider = hasValidImageUrl
|
||||||
? NetworkImage(imageUrl)
|
? NetworkImage(imageUrl)
|
||||||
: const AssetImage('lib/assets/images/default_avatar.png')
|
: const AssetImage('lib/assets/images/default_avatar.png')
|
||||||
as ImageProvider;
|
as ImageProvider;
|
||||||
@@ -72,10 +93,9 @@ class FriendDetailScreen extends StatelessWidget {
|
|||||||
elevation: 6, // Ombre sous l'app bar pour plus de profondeur
|
elevation: 6, // Ombre sous l'app bar pour plus de profondeur
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0), // Espacement autour du contenu
|
padding: const EdgeInsets.all(16), // Espacement autour du contenu
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
// Animation Hero pour une transition fluide lors de la navigation
|
// Animation Hero pour une transition fluide lors de la navigation
|
||||||
Hero(
|
Hero(
|
||||||
@@ -85,12 +105,12 @@ class FriendDetailScreen extends StatelessWidget {
|
|||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 80,
|
radius: 80,
|
||||||
backgroundImage: imageProvider,
|
backgroundImage: hasValidImageUrl ? imageProvider : null,
|
||||||
backgroundColor: Colors.grey.shade800,
|
backgroundColor: Colors.grey.shade800,
|
||||||
onBackgroundImageError: (error, stackTrace) {
|
onBackgroundImageError: (error, stackTrace) {
|
||||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $friendFirstName (ID: $friendId): $error');
|
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour $friendFirstName (ID: $friendId): $error');
|
||||||
},
|
},
|
||||||
child: imageUrl.isEmpty
|
child: !hasValidImageUrl
|
||||||
? const Icon(Icons.person, size: 60, color: Colors.white)
|
? const Icon(Icons.person, size: 60, color: Colors.white)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -156,9 +176,8 @@ class FriendDetailScreen extends StatelessWidget {
|
|||||||
/// Widget réutilisable pour afficher une ligne d'information avec un texte d'introduction et une valeur.
|
/// Widget réutilisable pour afficher une ligne d'information avec un texte d'introduction et une valeur.
|
||||||
Widget _buildInfoRow(String label, String value) {
|
Widget _buildInfoRow(String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12.0),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
|
|||||||
114
lib/presentation/widgets/friend_request_card.dart
Normal file
114
lib/presentation/widgets/friend_request_card.dart
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/friend_request.dart';
|
||||||
|
|
||||||
|
/// Widget pour afficher une demande d'amitié en attente.
|
||||||
|
/// Permet d'accepter ou de rejeter la demande (reçue) ou d'annuler (envoyée).
|
||||||
|
class FriendRequestCard extends StatelessWidget {
|
||||||
|
const FriendRequestCard({
|
||||||
|
required this.request,
|
||||||
|
required this.onAccept,
|
||||||
|
required this.onReject,
|
||||||
|
this.isSentRequest = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FriendRequest request;
|
||||||
|
final VoidCallback? onAccept;
|
||||||
|
final VoidCallback onReject;
|
||||||
|
final bool isSentRequest;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Avatar
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
backgroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: Text(
|
||||||
|
(isSentRequest ? request.friendFullName : request.userFullName).isNotEmpty
|
||||||
|
? (isSentRequest ? request.friendFullName : request.userFullName)[0].toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimaryContainer,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
// Informations
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isSentRequest
|
||||||
|
? (request.friendFullName.isNotEmpty
|
||||||
|
? request.friendFullName
|
||||||
|
: 'Utilisateur inconnu')
|
||||||
|
: (request.userFullName.isNotEmpty
|
||||||
|
? request.userFullName
|
||||||
|
: 'Utilisateur inconnu'),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
isSentRequest
|
||||||
|
? 'Demande envoyée'
|
||||||
|
: 'Demande d\'amitié',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Boutons d'action
|
||||||
|
if (isSentRequest)
|
||||||
|
// Pour les demandes envoyées : seulement bouton Annuler
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
color: Colors.orange,
|
||||||
|
onPressed: onReject,
|
||||||
|
tooltip: 'Annuler la demande',
|
||||||
|
)
|
||||||
|
else
|
||||||
|
// Pour les demandes reçues : Accepter et Rejeter
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Bouton Accepter
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.check_circle),
|
||||||
|
color: Colors.green,
|
||||||
|
onPressed: onAccept,
|
||||||
|
tooltip: 'Accepter',
|
||||||
|
),
|
||||||
|
// Bouton Rejeter
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.cancel),
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: onReject,
|
||||||
|
tooltip: 'Rejeter',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user