Bon checkpoint + Refactoring

This commit is contained in:
DahoudG
2024-11-08 20:30:23 +00:00
parent 19f6efa995
commit 1e888f41e8
21 changed files with 721 additions and 223 deletions

View File

@@ -1,5 +1,5 @@
class Urls { class Urls {
static const String baseUrl = 'http://192.168.1.11:8085'; static const String baseUrl = 'http://192.168.1.145:8085';
// Authentication and Users Endpoints // Authentication and Users Endpoints
static const String authenticateUser = '$baseUrl/users/authenticate'; static const String authenticateUser = '$baseUrl/users/authenticate';

View File

@@ -28,6 +28,71 @@ class EventRemoteDataSource {
} }
} }
/// Récupérer les événements créés par un utilisateur spécifique et ses amis.
/// Cette méthode envoie une requête POST au serveur pour obtenir la liste des événements créés
/// par l'utilisateur spécifié et ses amis, en utilisant l'identifiant de l'utilisateur.
///
/// [userId] : L'identifiant de l'utilisateur pour lequel récupérer les événements.
/// Retourne une liste de modèles d'événements [EventModel].
Future<List<EventModel>> getEventsCreatedByUserAndFriends(String userId) async {
// Log de début de la méthode pour signaler l'initialisation de la récupération des événements
print('[LOG] Démarrage de la récupération des événements créés par l\'utilisateur ID: $userId et ses amis.');
// Construction de l'URL de l'API pour la requête POST
final url = Uri.parse('${Urls.baseUrl}/events/created-by-user-and-friends');
print('[LOG] URL construite pour la requête: $url');
// Création de l'en-tête de la requête, spécifiant que le contenu est en JSON
final headers = {'Content-Type': 'application/json'};
print('[LOG] En-têtes de la requête: $headers');
// Construction du corps de la requête en JSON, incluant l'identifiant de l'utilisateur
final body = jsonEncode({'userId': userId});
print('[LOG] Corps de la requête JSON: $body');
// Envoi de la requête POST au serveur pour récupérer les événements
final response = await client.post(url, headers: headers, body: body);
print('[LOG] Requête POST envoyée au serveur.');
// Vérification et log de l'état de la réponse reçue
print('[LOG] Statut de la réponse HTTP: ${response.statusCode}');
// Gestion de la réponse en fonction du code de statut
if (response.statusCode == 200) {
// Déchiffrement du JSON reçu si le code de statut est 200 (OK)
final List<dynamic> jsonResponse = json.decode(response.body);
print('[LOG] Réponse JSON complète reçue (taille: ${jsonResponse.length}) :');
// Affichage détaillé de chaque événement
for (var i = 0; i < jsonResponse.length; i++) {
final event = jsonResponse[i];
print('[LOG] Événement $i :');
print(' - ID: ${event['id']}');
print(' - Titre: ${event['title']}');
print(' - Description: ${event['description']}');
print(' - Date de début: ${event['startDate']}');
print(' - Date de fin: ${event['endDate']}');
print(' - Localisation: ${event['location']}');
print(' - Catégorie: ${event['category']}');
print(' - Lien: ${event['link']}');
print(' - URL de l\'image: ${event['imageUrl']}');
print(' - Statut: ${event['status']}');
}
// Transformation du JSON en une liste d'objets EventModel
List<EventModel> events = jsonResponse.map((event) => EventModel.fromJson(event)).toList();
print('[LOG] Conversion JSON -> List<EventModel> réussie. Nombre d\'événements: ${events.length}');
// Retourne la liste d'événements si tout s'est bien passé
return events;
} else {
// Log et gestion de l'erreur en cas de statut HTTP autre que 200
print('[ERROR] Erreur lors de la récupération des événements: ${response.body}');
throw ServerException('[ERROR] Échec de récupération des événements créés par l\'utilisateur $userId et ses amis.');
}
}
/// Créer un nouvel événement via l'API. /// Créer un nouvel événement via l'API.
Future<EventModel> createEvent(EventModel event) async { Future<EventModel> createEvent(EventModel event) async {
print('Création d\'un nouvel événement avec les données: ${event.toJson()}'); print('Création d\'un nouvel événement avec les données: ${event.toJson()}');

View File

@@ -2,13 +2,13 @@ class EventModel {
final String id; final String id;
final String title; final String title;
final String description; final String description;
final String startDate; // Utiliser startDate au lieu de date, si c'est ce que l'API retourne final String startDate;
final String location; final String location;
final String category; final String category;
final String link; final String link;
final String? imageUrl; // Nullable final String? imageUrl;
final String creatorEmail; final String creatorEmail;
final List<dynamic> participants; // Si participants est une liste simple final List<dynamic> participants;
final String status; final String status;
final int reactionsCount; final int reactionsCount;
final int commentsCount; final int commentsCount;
@@ -32,25 +32,60 @@ class EventModel {
}); });
factory EventModel.fromJson(Map<String, dynamic> json) { factory EventModel.fromJson(Map<String, dynamic> json) {
print('[LOG] Création de l\'EventModel depuis JSON');
// Utiliser les valeurs par défaut si une clé est absente
final String id = json['id'] ?? 'ID Inconnu';
final String title = json['title'] ?? 'Titre Inconnu';
final String description = json['description'] ?? 'Description Inconnue';
final String startDate = json['startDate'] ?? 'Date de début Inconnue';
final String location = json['location'] ?? 'Localisation Inconnue';
final String category = json['category'] ?? 'Catégorie Inconnue';
final String link = json['link'] ?? 'Lien Inconnu';
final String? imageUrl = json['imageUrl'];
final String creatorEmail = json['creatorEmail'] ?? 'Email Inconnu';
final List<dynamic> participants = json['participants'] ?? [];
final String status = json['status'] ?? 'ouvert';
final int reactionsCount = json['reactionsCount'] ?? 0;
final int commentsCount = json['commentsCount'] ?? 0;
final int sharesCount = json['sharesCount'] ?? 0;
print('[LOG] Champs extraits depuis JSON :');
print(' - ID: $id');
print(' - Titre: $title');
print(' - Description: $description');
print(' - Date de début: $startDate');
print(' - Localisation: $location');
print(' - Catégorie: $category');
print(' - Lien: $link');
print(' - URL de l\'image: ${imageUrl ?? "Aucune"}');
print(' - Email du créateur: $creatorEmail');
print(' - Participants: ${participants.length} participants');
print(' - Statut: $status');
print(' - Nombre de réactions: $reactionsCount');
print(' - Nombre de commentaires: $commentsCount');
print(' - Nombre de partages: $sharesCount');
return EventModel( return EventModel(
id: json['id'], id: id,
title: json['title'], title: title,
description: json['description'], description: description,
startDate: json['startDate'], // Vérifier si c'est bien startDate startDate: startDate,
location: json['location'], location: location,
category: json['category'], category: category,
link: json['link'] ?? '', link: link,
imageUrl: json['imageUrl'], // Peut être null imageUrl: imageUrl,
creatorEmail: json['creatorEmail'], // Email du créateur creatorEmail: creatorEmail,
participants: json['participants'] ?? [], // Gérer les participants participants: participants,
status: json['status'] ?? 'ouvert', // Par défaut à "ouvert" si non fourni status: status,
reactionsCount: json['reactionsCount'] ?? 0, reactionsCount: reactionsCount,
commentsCount: json['commentsCount'] ?? 0, commentsCount: commentsCount,
sharesCount: json['sharesCount'] ?? 0, sharesCount: sharesCount,
); );
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
print('[LOG] Conversion de EventModel en JSON');
return { return {
'id': id, 'id': id,
'title': title, 'title': title,

View File

@@ -30,7 +30,11 @@ class FriendsProvider with ChangeNotifier {
/// [userId] : L'identifiant unique de l'utilisateur. /// [userId] : L'identifiant unique de l'utilisateur.
/// [loadMore] : Indique s'il s'agit d'une demande de chargement supplémentaire pour la pagination. /// [loadMore] : Indique s'il s'agit d'une demande de chargement supplémentaire pour la pagination.
/// ///
/// En cas d'erreur, logue l'exception et gère l'état `isLoading`. /// Cette méthode :
/// - Vérifie si un chargement est déjà en cours.
/// - Initialise ou poursuit la pagination.
/// - Exclut l'utilisateur lui-même de la liste.
/// - Gère les erreurs et logue chaque étape pour une traçabilité complète.
Future<void> fetchFriends(String userId, {bool loadMore = false}) async { Future<void> fetchFriends(String userId, {bool loadMore = false}) async {
if (_isLoading) { if (_isLoading) {
_logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.'); _logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.');
@@ -39,10 +43,10 @@ class FriendsProvider with ChangeNotifier {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
_logger.i('[LOG] Début du chargement des amis.'); _logger.i('[LOG] Début du chargement des amis pour l\'utilisateur $userId.');
// Réinitialisation uniquement si ce n'est pas un chargement supplémentaire
if (!loadMore) { if (!loadMore) {
// Réinitialisation de la liste et de la pagination si ce n'est pas un chargement supplémentaire
_friendsList = []; _friendsList = [];
_currentPage = 0; _currentPage = 0;
_hasMore = true; _hasMore = true;
@@ -57,9 +61,16 @@ class FriendsProvider with ChangeNotifier {
_hasMore = false; _hasMore = false;
_logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.'); _logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.');
} else { } else {
_friendsList.addAll(newFriends); for (var friend in newFriends) {
if (friend.friendId != userId) {
_friendsList.add(friend);
_logger.i("[LOG] Ajout de l'ami : ID = ${friend.friendId}, Nom = ${friend.friendFirstName} ${friend.friendLastName}");
} else {
_logger.w("[WARN] Exclusion de l'utilisateur lui-même de la liste d'amis : ${friend.friendId}");
}
}
_currentPage++; _currentPage++;
_logger.i('[LOG] Amis ajoutés à la liste. Page actuelle : $_currentPage'); _logger.i('[LOG] Page suivante préparée pour le prochain chargement, page actuelle : $_currentPage');
} }
} catch (e) { } catch (e) {
_logger.e('[ERROR] Erreur lors de la récupération des amis : $e'); _logger.e('[ERROR] Erreur lors de la récupération des amis : $e');

View File

@@ -17,26 +17,31 @@ class UserProvider with ChangeNotifier {
visitedPlacesCount: 0, visitedPlacesCount: 0,
); );
bool _isEmailDisplayedElsewhere = false; // Ajout de la propriété pour contrôler l'affichage de l'email
/// Getter pour l'objet utilisateur. /// Getter pour l'objet utilisateur.
User get user => _user; User get user => _user;
/// Getter pour vérifier si l'email est affiché ailleurs.
bool get isEmailDisplayedElsewhere => _isEmailDisplayedElsewhere;
/// Méthode pour définir l'état d'affichage de l'email.
void setEmailDisplayedElsewhere(bool value) {
_isEmailDisplayedElsewhere = value;
debugPrint("[LOG] isEmailDisplayedElsewhere mis à jour : $_isEmailDisplayedElsewhere");
notifyListeners();
}
/// Méthode pour définir les informations de l'utilisateur. /// Méthode pour définir les informations de l'utilisateur.
/// Logue les informations fournies et notifie les listeners des changements. /// Logue les informations fournies et notifie les listeners des changements.
///
/// [user] : L'objet utilisateur contenant toutes les informations.
void setUser(User user) { void setUser(User user) {
debugPrint("[LOG] Tentative de définition des informations de l'utilisateur : ${user.toString()}"); debugPrint("[LOG] Tentative de définition des informations de l'utilisateur : ${user.toString()}");
_user = user; _user = user;
debugPrint("[LOG] Informations utilisateur définies : ${_user.toString()}"); debugPrint("[LOG] Informations utilisateur définies : ${_user.toString()}");
// Notifie les widgets écoutant ce provider qu'une modification a eu lieu.
notifyListeners(); notifyListeners();
} }
/// Méthode pour mettre à jour des statistiques de l'utilisateur. /// Méthode pour mettre à jour des statistiques de l'utilisateur.
/// Cette méthode met à jour individuellement des attributs spécifiques comme le nombre d'amis ou d'événements.
void updateStatistics({ void updateStatistics({
int? eventsCount, int? eventsCount,
int? friendsCount, int? friendsCount,
@@ -59,12 +64,10 @@ class UserProvider with ChangeNotifier {
); );
debugPrint("[LOG] Nouvelles statistiques utilisateur : ${_user.toString()}"); debugPrint("[LOG] Nouvelles statistiques utilisateur : ${_user.toString()}");
notifyListeners(); notifyListeners();
} }
/// Méthode pour réinitialiser les informations de l'utilisateur. /// Méthode pour réinitialiser les informations de l'utilisateur.
/// Les valeurs sont loguées avant et après la réinitialisation.
void resetUser() { void resetUser() {
debugPrint("[LOG] Réinitialisation des informations de l'utilisateur."); debugPrint("[LOG] Réinitialisation des informations de l'utilisateur.");
debugPrint("[LOG] Valeurs avant réinitialisation : ${_user.toString()}"); debugPrint("[LOG] Valeurs avant réinitialisation : ${_user.toString()}");
@@ -83,7 +86,6 @@ class UserProvider with ChangeNotifier {
); );
debugPrint("[LOG] Informations utilisateur réinitialisées : ${_user.toString()}"); debugPrint("[LOG] Informations utilisateur réinitialisées : ${_user.toString()}");
notifyListeners(); notifyListeners();
} }
} }

View File

@@ -33,11 +33,15 @@ class FriendsRepositoryImpl implements FriendsRepository {
if (response.statusCode == 200) { if (response.statusCode == 200) {
_logger.i("[LOG] Liste des amis récupérée avec succès."); _logger.i("[LOG] Liste des amis récupérée avec succès.");
final List<dynamic> friendsJson = json.decode(response.body); final List<dynamic> friendsJson = json.decode(response.body);
_logger.i("[LOG] Nombre d'amis récupérés : ${friendsJson.length}"); _logger.i("[LOG] Nombre d'amis récupérés (excluant l'utilisateur lui-même) : ${friendsJson.length}");
return friendsJson.map((json) => Friend.fromJson(json as Map<String, dynamic>)).toList(); 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 { } else {
_logger.e("[ERROR] Échec de la récupération des amis. Code HTTP : ${response.statusCode}"); _logger.e("[ERROR] Échec de la récupération des amis. Code HTTP : ${response.statusCode}");
return []; return [];
@@ -56,7 +60,7 @@ class FriendsRepositoryImpl implements FriendsRepository {
@override @override
Future<void> addFriend(Friend friend) async { Future<void> addFriend(Friend friend) async {
try { try {
_logger.i("[LOG] Tentative d'ajout de l'ami : ${friend.firstName} ${friend.lastName}"); _logger.i("[LOG] Tentative d'ajout de l'ami : ${friend.friendFirstName} ${friend.friendLastName}");
final uri = Uri.parse('${Urls.baseUrl}/friends/send'); final uri = Uri.parse('${Urls.baseUrl}/friends/send');
final response = await client.post( final response = await client.post(

View File

@@ -12,8 +12,8 @@ enum FriendStatus { pending, accepted, blocked, unknown }
/// Chaque instance de [Friend] est immuable et toute modification doit passer par [copyWith]. /// Chaque instance de [Friend] est immuable et toute modification doit passer par [copyWith].
class Friend extends Equatable { class Friend extends Equatable {
final String friendId; // ID unique de l'ami, requis et non-nullable final String friendId; // ID unique de l'ami, requis et non-nullable
final String firstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données final String friendFirstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données
final String lastName; // Nom de famille, non-nullable final String friendLastName; // Nom de famille, non-nullable
final String? email; // Adresse e-mail, optionnelle mais typiquement présente final String? email; // Adresse e-mail, optionnelle mais typiquement présente
final String? imageUrl; // URL de l'image de profil, optionnelle final String? imageUrl; // URL de l'image de profil, optionnelle
final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown` final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown`
@@ -26,14 +26,14 @@ class Friend extends Equatable {
/// La validation des valeurs est incluse pour garantir l'intégrité des données. /// La validation des valeurs est incluse pour garantir l'intégrité des données.
Friend({ Friend({
required this.friendId, required this.friendId,
this.firstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides this.friendFirstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides
this.lastName = '', this.friendLastName = '',
this.email, this.email,
this.imageUrl, this.imageUrl,
this.status = FriendStatus.unknown, this.status = FriendStatus.unknown,
}) { }) {
assert(friendId.isNotEmpty, 'friendId ne doit pas être vide'); assert(friendId.isNotEmpty, 'friendId ne doit pas être vide');
_logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $firstName $lastName'); _logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $friendFirstName $friendLastName');
} }
/// Méthode factory pour créer un objet [Friend] à partir d'un JSON. /// Méthode factory pour créer un objet [Friend] à partir d'un JSON.
@@ -50,8 +50,8 @@ class Friend extends Equatable {
return Friend( return Friend(
friendId: json['friendId'] as String, friendId: json['friendId'] as String,
firstName: json['friendFirstName'] as String? ?? 'Ami inconnu', friendFirstName: json['friendFirstName'] as String? ?? 'Ami inconnu',
lastName: json['friendLastName'] as String? ?? '', friendLastName: json['friendLastName'] as String? ?? '',
email: json['email'] as String?, email: json['email'] as String?,
imageUrl: json['imageUrl'] as String?, imageUrl: json['imageUrl'] as String?,
status: _parseStatus(json['status'] as String?), status: _parseStatus(json['status'] as String?),
@@ -78,8 +78,8 @@ class Friend extends Equatable {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = { final json = {
'friendId': friendId, 'friendId': friendId,
'firstName': firstName, 'friendFirstName': friendFirstName,
'lastName': lastName, 'friendLastName': friendLastName,
'email': email, 'email': email,
'imageUrl': imageUrl, 'imageUrl': imageUrl,
'status': status.name, 'status': status.name,
@@ -94,16 +94,16 @@ class Friend extends Equatable {
/// Log chaque copie pour surveiller l'état des données. /// Log chaque copie pour surveiller l'état des données.
Friend copyWith({ Friend copyWith({
String? friendId, String? friendId,
String? firstName, String? friendFirstName,
String? lastName, String? friendLastName,
String? email, String? email,
String? imageUrl, String? imageUrl,
FriendStatus? status, FriendStatus? status,
}) { }) {
final newFriend = Friend( final newFriend = Friend(
friendId: friendId ?? this.friendId, friendId: friendId ?? this.friendId,
firstName: firstName ?? this.firstName, friendFirstName: friendFirstName ?? this.friendFirstName,
lastName: lastName ?? this.lastName, friendLastName: friendLastName ?? this.friendLastName,
email: email ?? this.email, email: email ?? this.email,
imageUrl: imageUrl ?? this.imageUrl, imageUrl: imageUrl ?? this.imageUrl,
status: status ?? this.status, status: status ?? this.status,
@@ -115,5 +115,5 @@ class Friend extends Equatable {
/// Propriétés utilisées pour comparer les objets [Friend], /// Propriétés utilisées pour comparer les objets [Friend],
/// facilitant l'utilisation dans des listes et des ensembles. /// facilitant l'utilisation dans des listes et des ensembles.
@override @override
List<Object?> get props => [friendId, firstName, lastName, email, imageUrl, status]; List<Object?> get props => [friendId, friendFirstName, friendLastName, email, imageUrl, status];
} }

View File

@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../state_management/event_bloc.dart'; import '../../state_management/event_bloc.dart';
import '../dialogs/add_event_dialog.dart'; import '../dialogs/add_event_dialog.dart';
/// Écran principal des événements, affichant une liste d'événements.
class EventScreen extends StatefulWidget { class EventScreen extends StatefulWidget {
final String userId; final String userId;
final String userFirstName; final String userFirstName;
@@ -63,9 +64,11 @@ class _EventScreenState extends State<EventScreen> {
body: BlocBuilder<EventBloc, EventState>( body: BlocBuilder<EventBloc, EventState>(
builder: (context, state) { builder: (context, state) {
if (state is EventLoading) { if (state is EventLoading) {
print('[LOG] Chargement en cours des événements...');
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (state is EventLoaded) { } else if (state is EventLoaded) {
final events = state.events; final events = state.events;
print('[LOG] Nombre d\'événements à afficher: ${events.length}');
if (events.isEmpty) { if (events.isEmpty) {
return const Center(child: Text('Aucun événement disponible.')); return const Center(child: Text('Aucun événement disponible.'));
} }
@@ -74,6 +77,7 @@ class _EventScreenState extends State<EventScreen> {
itemCount: events.length, itemCount: events.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final event = events[index]; final event = events[index];
print('[LOG] Affichage de l\'événement $index : ${event.title}');
return EventCard( return EventCard(
key: ValueKey(event.id), key: ValueKey(event.id),
event: event, event: event,
@@ -99,6 +103,7 @@ class _EventScreenState extends State<EventScreen> {
}, },
); );
} else if (state is EventError) { } else if (state is EventError) {
print('[ERROR] Message d\'erreur: ${state.message}');
return Center(child: Text('Erreur: ${state.message}')); return Center(child: Text('Erreur: ${state.message}'));
} }
return const Center(child: Text('Aucun événement disponible.')); return const Center(child: Text('Aucun événement disponible.'));

View File

@@ -45,11 +45,13 @@ class _FriendsScreenState extends State<FriendsScreen> {
/// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis. /// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis.
void _onScroll() { void _onScroll() {
final provider = Provider.of<FriendsProvider>(context, listen: false); final provider = Provider.of<FriendsProvider>(context, listen: false);
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent &&
// Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 &&
!provider.isLoading && provider.hasMore) { !provider.isLoading && provider.hasMore) {
debugPrint("[LOG] Scroll : fin de liste atteinte, chargement de la page suivante"); debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante.");
// Charger plus d'amis si on atteint la fin de la liste provider.fetchFriends(widget.userId, loadMore: true);
provider.fetchFriends(widget.userId);
} }
} }
@@ -65,10 +67,12 @@ class _FriendsScreenState extends State<FriendsScreen> {
IconButton( IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
onPressed: () { onPressed: () {
// Log de l'action de rafraîchissement if (!friendsProvider.isLoading) {
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis"); debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
// Rafraîchir la liste des amis friendsProvider.fetchFriends(widget.userId);
friendsProvider.fetchFriends(widget.userId); } else {
debugPrint("[LOG] Rafraîchissement en cours, action ignorée.");
}
}, },
), ),
], ],
@@ -110,27 +114,29 @@ class _FriendsScreenState extends State<FriendsScreen> {
mainAxisSpacing: 10, mainAxisSpacing: 10,
crossAxisSpacing: 10, crossAxisSpacing: 10,
), ),
itemCount: friendsProvider.friendsList.length, itemCount: friendsProvider.friendsList.length + (friendsProvider.isLoading && friendsProvider.hasMore ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= friendsProvider.friendsList.length) {
return const Center(child: CircularProgressIndicator());
}
final friend = friendsProvider.friendsList[index]; final friend = friendsProvider.friendsList[index];
debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}"); debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}");
return FriendsCircle( return FriendsCircle(
friend: friend, friend: friend,
onTap: () { onTap: () {
// Log pour l'action de visualisation des détails d'un ami
debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}"); debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}");
// Naviguer vers l'écran des détails de l'ami
FriendDetailScreen.open( FriendDetailScreen.open(
context, context,
friend.friendId, friend.friendId,
friend.firstName ?? 'Ami inconnu', friend.friendFirstName,
friend.imageUrl ?? '', friend.imageUrl ?? '',
); );
}, },
); );
}, },
); );
}, },
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../assets/animations/friend_expanding_card.dart'; import '../../../assets/animations/friend_expanding_card.dart';
import '../../../data/providers/friends_provider.dart'; import '../../../data/providers/friends_provider.dart';
@@ -7,10 +8,9 @@ import '../../widgets/friend_detail_screen.dart';
import '../../widgets/friends_appbar.dart'; import '../../widgets/friends_appbar.dart';
import '../../widgets/search_friends.dart'; import '../../widgets/search_friends.dart';
/// [FriendsScreenWithProvider] est un écran qui affiche la liste des amis.
/// Il utilise le provider [FriendsProvider] pour gérer les états et les données.
/// Chaque action est loguée pour permettre une traçabilité complète.
class FriendsScreenWithProvider extends StatelessWidget { class FriendsScreenWithProvider extends StatelessWidget {
final Logger _logger = Logger(); // Logger pour une meilleure traçabilité
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -29,6 +29,7 @@ class FriendsScreenWithProvider extends StatelessWidget {
final friends = friendsProvider.friendsList; final friends = friendsProvider.friendsList;
if (friends.isEmpty) { if (friends.isEmpty) {
_logger.i("[LOG] Aucun ami trouvé");
return const Center( return const Center(
child: Text( child: Text(
'Aucun ami trouvé', 'Aucun ami trouvé',
@@ -51,19 +52,22 @@ class FriendsScreenWithProvider extends StatelessWidget {
child: const Icon(Icons.delete, color: Colors.white), child: const Icon(Icons.delete, color: Colors.white),
), ),
onDismissed: (direction) { onDismissed: (direction) {
debugPrint("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}"); _logger.i("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}");
friendsProvider.removeFriend(friend.friendId); friendsProvider.removeFriend(friend.friendId);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")),
);
}, },
child: FriendExpandingCard( child: FriendExpandingCard(
name: friend.firstName ?? 'Ami inconnu', name: friend.friendFirstName ?? 'Ami inconnu',
imageUrl: friend.imageUrl ?? '', imageUrl: friend.imageUrl ?? '',
description: "Amis depuis ${friend.friendId}", description: "Amis depuis ${friend.friendId}",
onTap: () => _navigateToFriendDetail(context, friend), onTap: () => _navigateToFriendDetail(context, friend),
onMessageTap: () { onMessageTap: () {
debugPrint("[LOG] Envoi d'un message à l'ami : ${friend.firstName ?? 'Ami inconnu'}"); _logger.i("[LOG] Envoi d'un message à l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
}, },
onRemoveTap: () { onRemoveTap: () {
debugPrint("[LOG] Tentative de suppression de l'ami : ${friend.firstName ?? 'Ami inconnu'}"); _logger.i("[LOG] Tentative de suppression de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
friendsProvider.removeFriend(friend.friendId); friendsProvider.removeFriend(friend.friendId);
}, },
), ),
@@ -79,14 +83,13 @@ class FriendsScreenWithProvider extends StatelessWidget {
); );
} }
/// Navigue vers l'écran des détails de l'utilisateur (ami) récupéré via son `friendId`.
void _navigateToFriendDetail(BuildContext context, Friend friend) { void _navigateToFriendDetail(BuildContext context, Friend friend) {
debugPrint("[LOG] Navigation vers les détails de l'ami : ${friend.firstName ?? 'Ami inconnu'}"); _logger.i("[LOG] Navigation vers les détails de l'ami : ${friend.friendFirstName}");
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => FriendDetailScreen( builder: (context) => FriendDetailScreen(
name: friend.firstName ?? 'Ami inconnu', name: friend.friendFirstName,
imageUrl: friend.imageUrl ?? '', imageUrl: friend.imageUrl ?? '',
friendId: friend.friendId, // Passer l'ID pour récupérer les détails complets friendId: friend.friendId,
), ),
)); ));
} }

View File

@@ -11,7 +11,6 @@ import '../../widgets/statistics_section_card.dart';
import '../../widgets/support_section_card.dart'; import '../../widgets/support_section_card.dart';
import '../../widgets/user_info_card.dart'; import '../../widgets/user_info_card.dart';
class ProfileScreen extends StatelessWidget { class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key}); const ProfileScreen({super.key});

View File

@@ -65,10 +65,14 @@ class EventBloc extends Bloc<EventEvent, EventState> {
// Gestion du chargement des événements // Gestion du chargement des événements
Future<void> _onLoadEvents(LoadEvents event, Emitter<EventState> emit) async { Future<void> _onLoadEvents(LoadEvents event, Emitter<EventState> emit) async {
emit(EventLoading()); emit(EventLoading());
print('[LOG] Début du chargement des événements pour l\'utilisateur ${event.userId}');
try { try {
final events = await remoteDataSource.getAllEvents(); final events = await remoteDataSource.getEventsCreatedByUserAndFriends(event.userId);
print('[LOG] Événements chargés: ${events.length} éléments récupérés.');
emit(EventLoaded(events)); emit(EventLoaded(events));
} catch (e) { } catch (e) {
print('[ERROR] Erreur lors du chargement des événements: $e');
emit(EventError('Erreur lors du chargement des événements.')); emit(EventError('Erreur lors du chargement des événements.'));
} }
} }

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../core/constants/colors.dart';
/// [AccountDeletionCard] est un widget permettant à l'utilisateur de supprimer son compte.
/// Il affiche une confirmation avant d'effectuer l'action de suppression.
class AccountDeletionCard extends StatelessWidget { class AccountDeletionCard extends StatelessWidget {
final BuildContext context; final BuildContext context;
@@ -23,6 +25,7 @@ class AccountDeletionCard extends StatelessWidget {
); );
} }
/// Affiche un dialogue de confirmation pour la suppression du compte.
void _showDeleteConfirmationDialog() { void _showDeleteConfirmationDialog() {
showDialog( showDialog(
context: context, context: context,
@@ -39,14 +42,17 @@ class AccountDeletionCard extends StatelessWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () {
debugPrint("[LOG] Suppression du compte annulée.");
Navigator.of(context).pop();
},
child: Text('Annuler', style: TextStyle(color: AppColors.accentColor)), child: Text('Annuler', style: TextStyle(color: AppColors.accentColor)),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
print("[LOG] Suppression du compte confirmée."); debugPrint("[LOG] Suppression du compte confirmée.");
Navigator.of(context).pop(); Navigator.of(context).pop();
// Logique de suppression du compte // Logique de suppression du compte ici.
}, },
child: const Text( child: const Text(
'Supprimer', 'Supprimer',

View File

@@ -1,35 +1,98 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'custom_list_tile.dart';
import '../../../../core/constants/colors.dart'; import '../../../../core/constants/colors.dart';
/// [EditOptionsCard] permet à l'utilisateur d'accéder aux options d'édition du profil,
/// incluant la modification du profil, la photo et le mot de passe.
/// Les interactions sont entièrement loguées pour une traçabilité complète.
class EditOptionsCard extends StatelessWidget { class EditOptionsCard extends StatelessWidget {
const EditOptionsCard({Key? key}) : super(key: key); const EditOptionsCard({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("[LOG] Initialisation de EditOptionsCard");
return Card( return Card(
color: AppColors.cardColor, color: AppColors.cardColor.withOpacity(0.95),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2, elevation: 4,
shadowColor: AppColors.darkPrimary.withOpacity(0.3),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
CustomListTile( _buildOption(
context,
icon: Icons.edit, icon: Icons.edit,
label: 'Éditer le profil', label: 'Éditer le profil',
onTap: () => print("[LOG] Édition du profil."), logMessage: "Édition du profil",
onTap: () => debugPrint("[LOG] Édition du profil activée."),
), ),
CustomListTile( _buildDivider(),
_buildOption(
context,
icon: Icons.camera_alt, icon: Icons.camera_alt,
label: 'Changer la photo de profil', label: 'Changer la photo de profil',
onTap: () => print("[LOG] Changement de la photo de profil."), logMessage: "Changement de la photo de profil",
onTap: () =>
debugPrint("[LOG] Changement de la photo de profil activé."),
), ),
CustomListTile( _buildDivider(),
_buildOption(
context,
icon: Icons.lock, icon: Icons.lock,
label: 'Changer le mot de passe', label: 'Changer le mot de passe',
onTap: () => print("[LOG] Changement du mot de passe."), logMessage: "Changement du mot de passe",
onTap: () => debugPrint("[LOG] Changement du mot de passe activé."),
), ),
], ],
), ),
); );
} }
/// Construit chaque option de la carte avec une animation de feedback visuel.
Widget _buildOption(
BuildContext context, {
required IconData icon,
required String label,
required String logMessage,
required VoidCallback onTap,
}) {
return InkWell(
onTap: () {
debugPrint("[LOG] $logMessage");
onTap();
},
splashColor: AppColors.accentColor.withOpacity(0.3),
highlightColor: AppColors.accentColor.withOpacity(0.1),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
Icon(icon, color: AppColors.accentColor),
const SizedBox(width: 16),
Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
const Spacer(),
const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 16),
],
),
),
);
}
/// Construit un séparateur entre les options pour une meilleure structure visuelle.
Widget _buildDivider() {
return Divider(
color: Colors.white.withOpacity(0.2),
height: 1,
indent: 16,
endIndent: 16,
);
}
} }

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../core/constants/colors.dart';
class ExpandableSectionCard extends StatelessWidget { /// [ExpandableSectionCard] est une carte qui peut s'étendre pour révéler des éléments enfants.
/// Ce composant inclut des animations d'extension, des logs pour chaque action et une expérience utilisateur optimisée.
class ExpandableSectionCard extends StatefulWidget {
final String title; final String title;
final IconData icon; final IconData icon;
final List<Widget> children; final List<Widget> children;
@@ -13,25 +15,73 @@ class ExpandableSectionCard extends StatelessWidget {
required this.children, required this.children,
}) : super(key: key); }) : super(key: key);
@override
_ExpandableSectionCardState createState() => _ExpandableSectionCardState();
}
class _ExpandableSectionCardState extends State<ExpandableSectionCard> with SingleTickerProviderStateMixin {
bool _isExpanded = false;
late AnimationController _controller;
late Animation<double> _iconRotation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_iconRotation = Tween<double>(begin: 0, end: 0.5).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleExpansion() {
setState(() {
_isExpanded = !_isExpanded;
_isExpanded ? _controller.forward() : _controller.reverse();
debugPrint("[LOG] ${_isExpanded ? 'Ouverture' : 'Fermeture'} de l'ExpandableSectionCard : ${widget.title}");
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: AppColors.cardColor, color: AppColors.cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2, elevation: 3,
child: ExpansionTile( child: Column(
title: Text( children: [
title, ListTile(
style: const TextStyle( leading: Icon(widget.icon, color: AppColors.accentColor),
fontSize: 18, title: Text(
fontWeight: FontWeight.bold, widget.title,
color: Colors.white, style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
trailing: RotationTransition(
turns: _iconRotation,
child: Icon(Icons.expand_more, color: AppColors.accentColor),
),
onTap: _toggleExpansion,
), ),
), // Contenu de l'expansion
leading: Icon(icon, color: AppColors.accentColor), AnimatedCrossFade(
iconColor: AppColors.accentColor, duration: const Duration(milliseconds: 300),
collapsedIconColor: AppColors.accentColor, firstChild: Container(),
children: children, secondChild: Column(children: widget.children),
crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
),
],
), ),
); );
} }

View File

@@ -22,8 +22,8 @@ class FriendsCircle extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Combine firstName et lastName ou utilise "Ami inconnu" par défaut. // Combine firstName et lastName ou utilise "Ami inconnu" par défaut.
String displayName = [friend.firstName, friend.lastName] String displayName = [friend.friendFirstName, friend.friendLastName]
.where((namePart) => namePart.isNotEmpty) .where((namePart) => namePart != null && namePart.isNotEmpty)
.join(" ") .join(" ")
.trim(); .trim();
@@ -44,8 +44,10 @@ class FriendsCircle extends StatelessWidget {
child: CircleAvatar( child: CircleAvatar(
radius: 40, radius: 40,
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
? NetworkImage(friend.imageUrl!) // Utilise NetworkImage si l'URL est valide ? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau
: AssetImage('lib/assets/images/default_avatar.png') as ImageProvider, // Utilise AssetImage pour l'avatar par défaut ? NetworkImage(friend.imageUrl!)
: AssetImage(friend.imageUrl!) as ImageProvider) // Utilise AssetImage si c'est une ressource locale
: const AssetImage('lib/assets/images/default_avatar.png'), // Utilise AssetImage pour l'avatar par défaut
onBackgroundImageError: (error, stackTrace) { onBackgroundImageError: (error, stackTrace) {
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error'); _logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error');
}, },
@@ -71,3 +73,4 @@ class FriendsCircle extends StatelessWidget {
); );
} }
} }

View File

@@ -4,8 +4,9 @@ import '../../../../core/constants/colors.dart';
import '../../../../data/providers/user_provider.dart'; import '../../../../data/providers/user_provider.dart';
import '../../../../domain/entities/user.dart'; import '../../../../domain/entities/user.dart';
/// [ProfileHeader] est un widget qui affiche l'en-tête du profil utilisateur. /// [ProfileHeader] : Un widget d'en-tête de profil utilisateur visuellement amélioré
/// Comprend le nom de l'utilisateur, une image de fond, et un bouton de déconnexion avec confirmation. /// avec un gradient élégant, des animations, et un bouton de déconnexion stylisé.
/// Entièrement logué pour une traçabilité complète.
class ProfileHeader extends StatelessWidget { class ProfileHeader extends StatelessWidget {
final User user; final User user;
@@ -13,97 +14,157 @@ class ProfileHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("[LOG] Initialisation de ProfileHeader pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
return SliverAppBar( return SliverAppBar(
expandedHeight: 200.0, expandedHeight: 250.0,
floating: false, floating: false,
pinned: true, pinned: true,
elevation: 0,
backgroundColor: AppColors.darkPrimary, backgroundColor: AppColors.darkPrimary,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: _buildFlexibleSpaceBar(user),
title: Text( actions: [_buildLogoutButton(context)],
);
}
/// Construit un FlexibleSpaceBar avec un gradient et des animations.
/// Affiche le nom de l'utilisateur et l'image de profil avec un effet visuel enrichi.
Widget _buildFlexibleSpaceBar(User user) {
debugPrint("[LOG] Construction de FlexibleSpaceBar avec nom et image de profil.");
return FlexibleSpaceBar(
centerTitle: true,
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(12.0),
),
child: Text(
'Profil de ${user.userFirstName}', 'Profil de ${user.userFirstName}',
style: TextStyle( style: TextStyle(
color: AppColors.accentColor, color: AppColors.accentColor,
fontSize: 20.0, fontSize: 18.0,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
background: Image.network( ),
user.profileImageUrl, background: _buildProfileImageWithGradient(user.profileImageUrl),
);
}
/// Construit l'image de profil avec un overlay en gradient.
/// En cas d'erreur de chargement, affiche une image par défaut.
Widget _buildProfileImageWithGradient(String profileImageUrl) {
debugPrint("[LOG] Chargement de l'image de profil avec overlay de gradient.");
return Stack(
fit: StackFit.expand,
children: [
Image.network(
profileImageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator(color: AppColors.accentColor));
},
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
// Log en cas d'erreur de chargement de l'image debugPrint("[ERROR] Erreur lors du chargement de l'image de profil : $error");
print("[ERROR] Erreur lors du chargement de l'image de profil : $error"); return Image.asset(
return Image.asset('lib/assets/images/default_avatar.png', fit: BoxFit.cover); 'lib/assets/images/default_avatar.png',
fit: BoxFit.cover,
);
}, },
), ),
), Container(
actions: [ decoration: BoxDecoration(
IconButton( gradient: LinearGradient(
icon: const Icon(Icons.logout, color: Colors.white), colors: [Colors.transparent, AppColors.darkPrimary.withOpacity(0.8)],
onPressed: () { begin: Alignment.topCenter,
print("[LOG] Bouton de déconnexion cliqué."); // Log du clic du bouton de déconnexion end: Alignment.bottomCenter,
_showLogoutConfirmationDialog(context); // Affiche le dialogue de confirmation ),
}, ),
), ),
], ],
); );
} }
/// Construit un bouton de déconnexion stylisé avec animation.
/// Log chaque interaction pour assurer une traçabilité complète.
Widget _buildLogoutButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.logout, color: Colors.white),
splashRadius: 20,
onPressed: () {
debugPrint("[LOG] Clic sur le bouton de déconnexion.");
_showLogoutConfirmationDialog(context);
},
tooltip: 'Déconnexion',
);
}
/// Affiche une boîte de dialogue de confirmation pour la déconnexion. /// Affiche une boîte de dialogue de confirmation pour la déconnexion.
/// Log chaque action et résultat dans le terminal. /// Log chaque action et résultat pour une visibilité dans le terminal.
void _showLogoutConfirmationDialog(BuildContext context) { void _showLogoutConfirmationDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
// Log affichage du dialogue debugPrint("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion.");
print("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion.");
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
title: const Text( title: const Text(
'Confirmer la déconnexion', 'Confirmer la déconnexion',
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
), ),
content: const Text( content: const Text(
'Voulez-vous vraiment vous déconnecter ?', 'Voulez-vous vraiment vous déconnecter ?',
style: TextStyle(color: Colors.white70), style: TextStyle(color: Colors.white70, fontSize: 16),
), ),
actions: [ actions: [
// Bouton d'annulation de la déconnexion _buildCancelButton(context),
TextButton( _buildConfirmButton(context),
onPressed: () {
print("[LOG] Déconnexion annulée par l'utilisateur.");
Navigator.of(context).pop(); // Ferme le dialogue sans déconnecter
},
child: Text(
'Annuler',
style: TextStyle(color: AppColors.accentColor),
),
),
// Bouton de confirmation de la déconnexion
TextButton(
onPressed: () {
print("[LOG] Déconnexion confirmée."); // Log de la confirmation
Provider.of<UserProvider>(context, listen: false).resetUser(); // Réinitialise les infos utilisateur
print("[LOG] Informations utilisateur réinitialisées dans UserProvider.");
Navigator.of(context).pop(); // Ferme la boîte de dialogue
print("[LOG] Boîte de dialogue de confirmation fermée.");
Navigator.of(context).pushReplacementNamed('/'); // Redirige vers l'écran de connexion
print("[LOG] Redirection vers l'écran de connexion.");
},
child: const Text(
'Déconnexion',
style: TextStyle(color: Colors.redAccent),
),
),
], ],
); );
}, },
).then((_) { ).then((_) {
// Log lorsque le dialogue est fermé pour toute raison (confirmation ou annulation) debugPrint("[LOG] Fermeture de la boîte de dialogue de déconnexion.");
print("[LOG] La boîte de dialogue de confirmation de déconnexion est fermée.");
}); });
} }
/// Construit le bouton pour annuler la déconnexion avec log.
Widget _buildCancelButton(BuildContext context) {
return TextButton(
onPressed: () {
debugPrint("[LOG] L'utilisateur a annulé la déconnexion.");
Navigator.of(context).pop();
},
child: Text(
'Annuler',
style: TextStyle(color: AppColors.accentColor, fontWeight: FontWeight.bold),
),
);
}
/// Construit le bouton pour confirmer la déconnexion, avec log et réinitialisation des données utilisateur.
Widget _buildConfirmButton(BuildContext context) {
return TextButton(
onPressed: () {
debugPrint("[LOG] L'utilisateur a confirmé la déconnexion.");
// Réinitialisation des informations de l'utilisateur
Provider.of<UserProvider>(context, listen: false).resetUser();
debugPrint("[LOG] Informations utilisateur réinitialisées dans UserProvider.");
Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/'); // Redirection vers l'écran de connexion
debugPrint("[LOG] Redirection vers l'écran de connexion.");
},
child: const Text(
'Déconnexion',
style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
),
);
}
} }

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/constants/colors.dart'; import '../../../../core/constants/colors.dart';
/// [StatTile] affiche une statistique utilisateur avec une icône, un label et une valeur.
/// Ce composant inclut des animations et une traçabilité des interactions.
class StatTile extends StatelessWidget { class StatTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
@@ -15,17 +17,40 @@ class StatTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( debugPrint("[LOG] Initialisation de StatTile pour la statistique : $label");
leading: Icon(icon, color: AppColors.accentColor),
title: Text(label, style: const TextStyle(color: Colors.white)), return TweenAnimationBuilder<double>(
trailing: Text( duration: const Duration(milliseconds: 500),
value, tween: Tween<double>(begin: 0.9, end: 1.0),
style: const TextStyle( curve: Curves.easeOutBack,
color: Colors.white, builder: (context, scale, child) {
fontWeight: FontWeight.bold, return Transform.scale(
fontSize: 16, scale: scale,
), child: ListTile(
), leading: Icon(
icon,
color: AppColors.accentColor,
size: 28,
),
title: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
trailing: Text(
value,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
);
},
); );
} }
} }

View File

@@ -3,6 +3,8 @@ import '../../../../core/constants/colors.dart';
import '../../../../domain/entities/user.dart'; import '../../../../domain/entities/user.dart';
import 'stat_tile.dart'; import 'stat_tile.dart';
/// [StatisticsSectionCard] affiche les statistiques principales de l'utilisateur avec des animations.
/// Ce composant est optimisé pour une expérience interactive et une traçabilité complète des actions via les logs.
class StatisticsSectionCard extends StatelessWidget { class StatisticsSectionCard extends StatelessWidget {
final User user; final User user;
@@ -10,10 +12,13 @@ class StatisticsSectionCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("[LOG] Initialisation de StatisticsSectionCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
return Card( return Card(
color: AppColors.cardColor, color: AppColors.cardColor.withOpacity(0.95),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 2, elevation: 5,
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@@ -28,13 +33,73 @@ class StatisticsSectionCard extends StatelessWidget {
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
StatTile(icon: Icons.event, label: 'Événements Participés', value: '${user.eventsCount}'), // Liste des statistiques avec animations
StatTile(icon: Icons.place, label: 'Établissements Visités', value: '${user.visitedPlacesCount}'), _buildAnimatedStatTile(
StatTile(icon: Icons.post_add, label: 'Publications', value: '${user.postsCount}'), icon: Icons.event,
StatTile(icon: Icons.group, label: 'Amis/Followers', value: '${user.friendsCount}'), label: 'Événements Participés',
value: '${user.eventsCount}',
logMessage: "Affichage des événements participés : ${user.eventsCount}",
),
_buildDivider(),
_buildAnimatedStatTile(
icon: Icons.place,
label: 'Établissements Visités',
value: '${user.visitedPlacesCount}',
logMessage: "Affichage des établissements visités : ${user.visitedPlacesCount}",
),
_buildDivider(),
_buildAnimatedStatTile(
icon: Icons.post_add,
label: 'Publications',
value: '${user.postsCount}',
logMessage: "Affichage des publications : ${user.postsCount}",
),
_buildDivider(),
_buildAnimatedStatTile(
icon: Icons.group,
label: 'Amis/Followers',
value: '${user.friendsCount}',
logMessage: "Affichage des amis/followers : ${user.friendsCount}",
),
], ],
), ),
), ),
); );
} }
/// Construit chaque `StatTile` avec une animation de transition en fondu et logue chaque statistique.
Widget _buildAnimatedStatTile({
required IconData icon,
required String label,
required String value,
required String logMessage,
}) {
debugPrint("[LOG] $logMessage");
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 500),
tween: Tween<double>(begin: 0, end: 1),
curve: Curves.easeOut,
builder: (context, opacity, child) {
return Opacity(
opacity: opacity,
child: StatTile(
icon: icon,
label: label,
value: value,
),
);
},
);
}
/// Construit un séparateur visuel entre chaque statistique.
Widget _buildDivider() {
return Divider(
color: Colors.white.withOpacity(0.2),
height: 1,
indent: 16,
endIndent: 16,
);
}
} }

View File

@@ -1,46 +1,107 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/constants/colors.dart'; import '../../../../core/constants/colors.dart';
import 'custom_list_tile.dart';
/// [SupportSectionCard] affiche les options de support et assistance.
/// Inclut des animations, du retour haptique, et des logs détaillés pour chaque action.
class SupportSectionCard extends StatelessWidget { class SupportSectionCard extends StatelessWidget {
const SupportSectionCard({Key? key}) : super(key: key); const SupportSectionCard({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("[LOG] Initialisation de SupportSectionCard.");
return Card( return Card(
color: AppColors.cardColor, color: AppColors.cardColor.withOpacity(0.95),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2, elevation: 6,
child: Column( shadowColor: AppColors.darkPrimary.withOpacity(0.4),
children: [ child: Padding(
const Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
padding: EdgeInsets.all(16.0), child: Column(
child: Text( crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Support et Assistance', 'Support et Assistance',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
letterSpacing: 1.1,
), ),
), ),
), const SizedBox(height: 10),
CustomListTile( _buildOption(
icon: Icons.help, context,
label: 'Support et Assistance', icon: Icons.help_outline,
onTap: () => print("[LOG] Accès au Support et Assistance."), label: 'Support et Assistance',
), logMessage: "Accès au Support et Assistance.",
CustomListTile( ),
icon: Icons.article, _buildDivider(),
label: 'Conditions d\'utilisation', _buildOption(
onTap: () => print("[LOG] Accès aux conditions d'utilisation."), context,
), icon: Icons.article_outlined,
CustomListTile( label: 'Conditions d\'utilisation',
icon: Icons.privacy_tip, logMessage: "Accès aux conditions d'utilisation.",
label: 'Politique de confidentialité', ),
onTap: () => print("[LOG] Accès à la politique de confidentialité."), _buildDivider(),
), _buildOption(
], context,
icon: Icons.privacy_tip_outlined,
label: 'Politique de confidentialité',
logMessage: "Accès à la politique de confidentialité.",
),
],
),
), ),
); );
} }
/// Construit chaque option de support avec une animation de feedback visuel.
Widget _buildOption(
BuildContext context, {
required IconData icon,
required String label,
required String logMessage,
}) {
return InkWell(
onTap: () {
HapticFeedback.lightImpact(); // Retour haptique léger
debugPrint("[LOG] $logMessage");
// Ajout de la navigation ou de l'action ici.
},
splashColor: AppColors.accentColor.withOpacity(0.3),
highlightColor: AppColors.cardColor.withOpacity(0.1),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
children: [
Icon(icon, color: AppColors.accentColor, size: 28),
const SizedBox(width: 15),
Expanded(
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const Icon(Icons.chevron_right, color: Colors.white70),
],
),
),
);
}
/// Construit un séparateur entre les options pour une meilleure structure visuelle.
Widget _buildDivider() {
return Divider(
color: Colors.white.withOpacity(0.2),
height: 1,
indent: 16,
endIndent: 16,
);
}
} }

View File

@@ -1,7 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/constants/colors.dart'; import '../../../../core/constants/colors.dart';
import '../../../../domain/entities/user.dart'; import '../../../../domain/entities/user.dart';
import '../../data/providers/user_provider.dart';
/// [UserInfoCard] affiche les informations essentielles de l'utilisateur de manière concise.
/// Conçu pour minimiser les répétitions tout en garantissant une expérience utilisateur fluide.
class UserInfoCard extends StatelessWidget { class UserInfoCard extends StatelessWidget {
final User user; final User user;
@@ -9,38 +13,64 @@ class UserInfoCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("[LOG] Initialisation de UserInfoCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
return Card( return Card(
color: AppColors.cardColor, color: AppColors.cardColor.withOpacity(0.9),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 2, elevation: 5,
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
CircleAvatar( TweenAnimationBuilder<double>(
radius: 50, duration: const Duration(milliseconds: 600),
backgroundImage: NetworkImage(user.profileImageUrl), tween: Tween<double>(begin: 0, end: 1),
backgroundColor: Colors.transparent, curve: Curves.elasticOut,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(user.profileImageUrl),
backgroundColor: Colors.transparent,
onBackgroundImageError: (error, stackTrace) {
debugPrint("[ERROR] Erreur de chargement de l'image de profil : $error");
},
child: child,
),
);
},
child: Icon(Icons.person, size: 50, color: Colors.grey.shade300),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'${user.userFirstName} ${user.userLastName}', '${user.userFirstName} ${user.userLastName}',
style: const TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: AppColors.accentColor,
letterSpacing: 1.2, letterSpacing: 1.2,
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( if (!context.select((UserProvider provider) => provider.isEmailDisplayedElsewhere)) // Afficher seulement si non affiché ailleurs
user.email, GestureDetector(
style: TextStyle( onTap: () {
fontSize: 14, debugPrint("[LOG] Clic sur l'email de l'utilisateur : ${user.email}");
color: Colors.grey[600], },
decoration: TextDecoration.underline, child: Text(
user.email,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade300,
decoration: TextDecoration.underline,
decorationColor: AppColors.accentColor.withOpacity(0.5),
),
),
), ),
),
], ],
), ),
), ),