Bon checkpoint + Refactoring
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
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
|
||||
static const String authenticateUser = '$baseUrl/users/authenticate';
|
||||
|
||||
@@ -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.
|
||||
Future<EventModel> createEvent(EventModel event) async {
|
||||
print('Création d\'un nouvel événement avec les données: ${event.toJson()}');
|
||||
|
||||
@@ -2,13 +2,13 @@ class EventModel {
|
||||
final String id;
|
||||
final String title;
|
||||
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 category;
|
||||
final String link;
|
||||
final String? imageUrl; // Nullable
|
||||
final String? imageUrl;
|
||||
final String creatorEmail;
|
||||
final List<dynamic> participants; // Si participants est une liste simple
|
||||
final List<dynamic> participants;
|
||||
final String status;
|
||||
final int reactionsCount;
|
||||
final int commentsCount;
|
||||
@@ -32,25 +32,60 @@ class EventModel {
|
||||
});
|
||||
|
||||
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(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
description: json['description'],
|
||||
startDate: json['startDate'], // Vérifier si c'est bien startDate
|
||||
location: json['location'],
|
||||
category: json['category'],
|
||||
link: json['link'] ?? '',
|
||||
imageUrl: json['imageUrl'], // Peut être null
|
||||
creatorEmail: json['creatorEmail'], // Email du créateur
|
||||
participants: json['participants'] ?? [], // Gérer les participants
|
||||
status: json['status'] ?? 'ouvert', // Par défaut à "ouvert" si non fourni
|
||||
reactionsCount: json['reactionsCount'] ?? 0,
|
||||
commentsCount: json['commentsCount'] ?? 0,
|
||||
sharesCount: json['sharesCount'] ?? 0,
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
startDate: startDate,
|
||||
location: location,
|
||||
category: category,
|
||||
link: link,
|
||||
imageUrl: imageUrl,
|
||||
creatorEmail: creatorEmail,
|
||||
participants: participants,
|
||||
status: status,
|
||||
reactionsCount: reactionsCount,
|
||||
commentsCount: commentsCount,
|
||||
sharesCount: sharesCount,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
print('[LOG] Conversion de EventModel en JSON');
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
|
||||
@@ -30,7 +30,11 @@ class FriendsProvider with ChangeNotifier {
|
||||
/// [userId] : L'identifiant unique de l'utilisateur.
|
||||
/// [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 {
|
||||
if (_isLoading) {
|
||||
_logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.');
|
||||
@@ -39,10 +43,10 @@ class FriendsProvider with ChangeNotifier {
|
||||
|
||||
_isLoading = true;
|
||||
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) {
|
||||
// Réinitialisation de la liste et de la pagination si ce n'est pas un chargement supplémentaire
|
||||
_friendsList = [];
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
@@ -57,9 +61,16 @@ class FriendsProvider with ChangeNotifier {
|
||||
_hasMore = false;
|
||||
_logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.');
|
||||
} 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++;
|
||||
_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) {
|
||||
_logger.e('[ERROR] Erreur lors de la récupération des amis : $e');
|
||||
|
||||
@@ -17,26 +17,31 @@ class UserProvider with ChangeNotifier {
|
||||
visitedPlacesCount: 0,
|
||||
);
|
||||
|
||||
bool _isEmailDisplayedElsewhere = false; // Ajout de la propriété pour contrôler l'affichage de l'email
|
||||
|
||||
/// Getter pour l'objet utilisateur.
|
||||
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.
|
||||
/// Logue les informations fournies et notifie les listeners des changements.
|
||||
///
|
||||
/// [user] : L'objet utilisateur contenant toutes les informations.
|
||||
void setUser(User user) {
|
||||
debugPrint("[LOG] Tentative de définition des informations de l'utilisateur : ${user.toString()}");
|
||||
|
||||
_user = user;
|
||||
|
||||
debugPrint("[LOG] Informations utilisateur définies : ${_user.toString()}");
|
||||
|
||||
// Notifie les widgets écoutant ce provider qu'une modification a eu lieu.
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 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({
|
||||
int? eventsCount,
|
||||
int? friendsCount,
|
||||
@@ -59,12 +64,10 @@ class UserProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
debugPrint("[LOG] Nouvelles statistiques utilisateur : ${_user.toString()}");
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Méthode pour réinitialiser les informations de l'utilisateur.
|
||||
/// Les valeurs sont loguées avant et après la réinitialisation.
|
||||
void resetUser() {
|
||||
debugPrint("[LOG] Réinitialisation des informations de l'utilisateur.");
|
||||
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()}");
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,15 @@ class FriendsRepositoryImpl implements FriendsRepository {
|
||||
|
||||
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 : ${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 {
|
||||
_logger.e("[ERROR] Échec de la récupération des amis. Code HTTP : ${response.statusCode}");
|
||||
return [];
|
||||
@@ -56,7 +60,7 @@ class FriendsRepositoryImpl implements FriendsRepository {
|
||||
@override
|
||||
Future<void> addFriend(Friend friend) async {
|
||||
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 response = await client.post(
|
||||
|
||||
@@ -12,8 +12,8 @@ enum FriendStatus { pending, accepted, blocked, unknown }
|
||||
/// Chaque instance de [Friend] est immuable et toute modification doit passer par [copyWith].
|
||||
class Friend extends Equatable {
|
||||
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 lastName; // Nom de famille, non-nullable
|
||||
final String friendFirstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données
|
||||
final String friendLastName; // Nom de famille, non-nullable
|
||||
final String? email; // Adresse e-mail, optionnelle mais typiquement présente
|
||||
final String? imageUrl; // URL de l'image de profil, optionnelle
|
||||
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.
|
||||
Friend({
|
||||
required this.friendId,
|
||||
this.firstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides
|
||||
this.lastName = '',
|
||||
this.friendFirstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides
|
||||
this.friendLastName = '',
|
||||
this.email,
|
||||
this.imageUrl,
|
||||
this.status = FriendStatus.unknown,
|
||||
}) {
|
||||
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.
|
||||
@@ -50,8 +50,8 @@ class Friend extends Equatable {
|
||||
|
||||
return Friend(
|
||||
friendId: json['friendId'] as String,
|
||||
firstName: json['friendFirstName'] as String? ?? 'Ami inconnu',
|
||||
lastName: json['friendLastName'] as String? ?? '',
|
||||
friendFirstName: json['friendFirstName'] as String? ?? 'Ami inconnu',
|
||||
friendLastName: json['friendLastName'] as String? ?? '',
|
||||
email: json['email'] as String?,
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
status: _parseStatus(json['status'] as String?),
|
||||
@@ -78,8 +78,8 @@ class Friend extends Equatable {
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'friendId': friendId,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'friendFirstName': friendFirstName,
|
||||
'friendLastName': friendLastName,
|
||||
'email': email,
|
||||
'imageUrl': imageUrl,
|
||||
'status': status.name,
|
||||
@@ -94,16 +94,16 @@ class Friend extends Equatable {
|
||||
/// Log chaque copie pour surveiller l'état des données.
|
||||
Friend copyWith({
|
||||
String? friendId,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? friendFirstName,
|
||||
String? friendLastName,
|
||||
String? email,
|
||||
String? imageUrl,
|
||||
FriendStatus? status,
|
||||
}) {
|
||||
final newFriend = Friend(
|
||||
friendId: friendId ?? this.friendId,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
friendFirstName: friendFirstName ?? this.friendFirstName,
|
||||
friendLastName: friendLastName ?? this.friendLastName,
|
||||
email: email ?? this.email,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
status: status ?? this.status,
|
||||
@@ -115,5 +115,5 @@ class Friend extends Equatable {
|
||||
/// Propriétés utilisées pour comparer les objets [Friend],
|
||||
/// facilitant l'utilisation dans des listes et des ensembles.
|
||||
@override
|
||||
List<Object?> get props => [friendId, firstName, lastName, email, imageUrl, status];
|
||||
List<Object?> get props => [friendId, friendFirstName, friendLastName, email, imageUrl, status];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../state_management/event_bloc.dart';
|
||||
import '../dialogs/add_event_dialog.dart';
|
||||
|
||||
/// Écran principal des événements, affichant une liste d'événements.
|
||||
class EventScreen extends StatefulWidget {
|
||||
final String userId;
|
||||
final String userFirstName;
|
||||
@@ -63,9 +64,11 @@ class _EventScreenState extends State<EventScreen> {
|
||||
body: BlocBuilder<EventBloc, EventState>(
|
||||
builder: (context, state) {
|
||||
if (state is EventLoading) {
|
||||
print('[LOG] Chargement en cours des événements...');
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is EventLoaded) {
|
||||
final events = state.events;
|
||||
print('[LOG] Nombre d\'événements à afficher: ${events.length}');
|
||||
if (events.isEmpty) {
|
||||
return const Center(child: Text('Aucun événement disponible.'));
|
||||
}
|
||||
@@ -74,6 +77,7 @@ class _EventScreenState extends State<EventScreen> {
|
||||
itemCount: events.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = events[index];
|
||||
print('[LOG] Affichage de l\'événement $index : ${event.title}');
|
||||
return EventCard(
|
||||
key: ValueKey(event.id),
|
||||
event: event,
|
||||
@@ -99,6 +103,7 @@ class _EventScreenState extends State<EventScreen> {
|
||||
},
|
||||
);
|
||||
} else if (state is EventError) {
|
||||
print('[ERROR] Message d\'erreur: ${state.message}');
|
||||
return Center(child: Text('Erreur: ${state.message}'));
|
||||
}
|
||||
return const Center(child: Text('Aucun événement disponible.'));
|
||||
|
||||
@@ -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.
|
||||
void _onScroll() {
|
||||
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) {
|
||||
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);
|
||||
debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante.");
|
||||
provider.fetchFriends(widget.userId, loadMore: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +67,12 @@ class _FriendsScreenState extends State<FriendsScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
// Log de l'action de rafraîchissement
|
||||
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
|
||||
// Rafraîchir la liste des amis
|
||||
friendsProvider.fetchFriends(widget.userId);
|
||||
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.");
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -110,27 +114,29 @@ class _FriendsScreenState extends State<FriendsScreen> {
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
),
|
||||
itemCount: friendsProvider.friendsList.length,
|
||||
itemCount: friendsProvider.friendsList.length + (friendsProvider.isLoading && friendsProvider.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= friendsProvider.friendsList.length) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final friend = friendsProvider.friendsList[index];
|
||||
debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}");
|
||||
|
||||
return FriendsCircle(
|
||||
friend: friend,
|
||||
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}");
|
||||
// Naviguer vers l'écran des détails de l'ami
|
||||
FriendDetailScreen.open(
|
||||
context,
|
||||
friend.friendId,
|
||||
friend.firstName ?? 'Ami inconnu',
|
||||
friend.friendFirstName,
|
||||
friend.imageUrl ?? '',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../assets/animations/friend_expanding_card.dart';
|
||||
import '../../../data/providers/friends_provider.dart';
|
||||
@@ -7,10 +8,9 @@ import '../../widgets/friend_detail_screen.dart';
|
||||
import '../../widgets/friends_appbar.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 {
|
||||
final Logger _logger = Logger(); // Logger pour une meilleure traçabilité
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -29,6 +29,7 @@ class FriendsScreenWithProvider extends StatelessWidget {
|
||||
final friends = friendsProvider.friendsList;
|
||||
|
||||
if (friends.isEmpty) {
|
||||
_logger.i("[LOG] Aucun ami trouvé");
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucun ami trouvé',
|
||||
@@ -51,19 +52,22 @@ class FriendsScreenWithProvider extends StatelessWidget {
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
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);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")),
|
||||
);
|
||||
},
|
||||
child: FriendExpandingCard(
|
||||
name: friend.firstName ?? 'Ami inconnu',
|
||||
name: friend.friendFirstName ?? 'Ami inconnu',
|
||||
imageUrl: friend.imageUrl ?? '',
|
||||
description: "Amis depuis ${friend.friendId}",
|
||||
onTap: () => _navigateToFriendDetail(context, friend),
|
||||
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: () {
|
||||
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);
|
||||
},
|
||||
),
|
||||
@@ -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) {
|
||||
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(
|
||||
builder: (context) => FriendDetailScreen(
|
||||
name: friend.firstName ?? 'Ami inconnu',
|
||||
name: friend.friendFirstName,
|
||||
imageUrl: friend.imageUrl ?? '',
|
||||
friendId: friend.friendId, // Passer l'ID pour récupérer les détails complets
|
||||
friendId: friend.friendId,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../../widgets/statistics_section_card.dart';
|
||||
import '../../widgets/support_section_card.dart';
|
||||
import '../../widgets/user_info_card.dart';
|
||||
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
|
||||
@@ -65,10 +65,14 @@ class EventBloc extends Bloc<EventEvent, EventState> {
|
||||
// Gestion du chargement des événements
|
||||
Future<void> _onLoadEvents(LoadEvents event, Emitter<EventState> emit) async {
|
||||
emit(EventLoading());
|
||||
print('[LOG] Début du chargement des événements pour l\'utilisateur ${event.userId}');
|
||||
|
||||
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));
|
||||
} catch (e) {
|
||||
print('[ERROR] Erreur lors du chargement des événements: $e');
|
||||
emit(EventError('Erreur lors du chargement des événements.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.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 {
|
||||
final BuildContext context;
|
||||
|
||||
@@ -23,6 +25,7 @@ class AccountDeletionCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche un dialogue de confirmation pour la suppression du compte.
|
||||
void _showDeleteConfirmationDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -39,14 +42,17 @@ class AccountDeletionCard extends StatelessWidget {
|
||||
),
|
||||
actions: [
|
||||
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)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
print("[LOG] Suppression du compte confirmée.");
|
||||
debugPrint("[LOG] Suppression du compte confirmée.");
|
||||
Navigator.of(context).pop();
|
||||
// Logique de suppression du compte
|
||||
// Logique de suppression du compte ici.
|
||||
},
|
||||
child: const Text(
|
||||
'Supprimer',
|
||||
|
||||
@@ -1,35 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'custom_list_tile.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 {
|
||||
const EditOptionsCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de EditOptionsCard");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
color: AppColors.cardColor.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 4,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.3),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomListTile(
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.edit,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.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 IconData icon;
|
||||
final List<Widget> children;
|
||||
@@ -13,25 +15,73 @@ class ExpandableSectionCard extends StatelessWidget {
|
||||
required this.children,
|
||||
}) : 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
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
child: ExpansionTile(
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
elevation: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(widget.icon, color: AppColors.accentColor),
|
||||
title: Text(
|
||||
widget.title,
|
||||
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,
|
||||
),
|
||||
),
|
||||
leading: Icon(icon, color: AppColors.accentColor),
|
||||
iconColor: AppColors.accentColor,
|
||||
collapsedIconColor: AppColors.accentColor,
|
||||
children: children,
|
||||
// Contenu de l'expansion
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
firstChild: Container(),
|
||||
secondChild: Column(children: widget.children),
|
||||
crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ class FriendsCircle extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Combine firstName et lastName ou utilise "Ami inconnu" par défaut.
|
||||
String displayName = [friend.firstName, friend.lastName]
|
||||
.where((namePart) => namePart.isNotEmpty)
|
||||
String displayName = [friend.friendFirstName, friend.friendLastName]
|
||||
.where((namePart) => namePart != null && namePart.isNotEmpty)
|
||||
.join(" ")
|
||||
.trim();
|
||||
|
||||
@@ -44,8 +44,10 @@ class FriendsCircle extends StatelessWidget {
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
|
||||
? NetworkImage(friend.imageUrl!) // Utilise NetworkImage si l'URL est valide
|
||||
: AssetImage('lib/assets/images/default_avatar.png') as ImageProvider, // Utilise AssetImage pour l'avatar par défaut
|
||||
? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau
|
||||
? 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) {
|
||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error');
|
||||
},
|
||||
@@ -71,3 +73,4 @@ class FriendsCircle extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import '../../../../core/constants/colors.dart';
|
||||
import '../../../../data/providers/user_provider.dart';
|
||||
import '../../../../domain/entities/user.dart';
|
||||
|
||||
/// [ProfileHeader] est un widget qui affiche l'en-tête du profil utilisateur.
|
||||
/// Comprend le nom de l'utilisateur, une image de fond, et un bouton de déconnexion avec confirmation.
|
||||
/// [ProfileHeader] : Un widget d'en-tête de profil utilisateur visuellement amélioré
|
||||
/// 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 {
|
||||
final User user;
|
||||
|
||||
@@ -13,97 +14,157 @@ class ProfileHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de ProfileHeader pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
|
||||
|
||||
return SliverAppBar(
|
||||
expandedHeight: 200.0,
|
||||
expandedHeight: 250.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColors.darkPrimary,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: Text(
|
||||
flexibleSpace: _buildFlexibleSpaceBar(user),
|
||||
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}',
|
||||
style: TextStyle(
|
||||
color: AppColors.accentColor,
|
||||
fontSize: 20.0,
|
||||
fontSize: 18.0,
|
||||
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,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(child: CircularProgressIndicator(color: AppColors.accentColor));
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// Log en cas d'erreur de chargement de l'image
|
||||
print("[ERROR] Erreur lors du chargement de l'image de profil : $error");
|
||||
return Image.asset('lib/assets/images/default_avatar.png', fit: BoxFit.cover);
|
||||
debugPrint("[ERROR] Erreur lors du chargement de l'image de profil : $error");
|
||||
return Image.asset(
|
||||
'lib/assets/images/default_avatar.png',
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
onPressed: () {
|
||||
print("[LOG] Bouton de déconnexion cliqué."); // Log du clic du bouton de déconnexion
|
||||
_showLogoutConfirmationDialog(context); // Affiche le dialogue de confirmation
|
||||
},
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.transparent, AppColors.darkPrimary.withOpacity(0.8)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// Log affichage du dialogue
|
||||
print("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion.");
|
||||
debugPrint("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion.");
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
backgroundColor: AppColors.backgroundColor,
|
||||
title: const Text(
|
||||
'Confirmer la déconnexion',
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: const Text(
|
||||
'Voulez-vous vraiment vous déconnecter ?',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
// Bouton d'annulation de la déconnexion
|
||||
TextButton(
|
||||
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),
|
||||
),
|
||||
),
|
||||
_buildCancelButton(context),
|
||||
_buildConfirmButton(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((_) {
|
||||
// Log lorsque le dialogue est fermé pour toute raison (confirmation ou annulation)
|
||||
print("[LOG] La boîte de dialogue de confirmation de déconnexion est fermée.");
|
||||
debugPrint("[LOG] Fermeture de la boîte de dialogue de déconnexion.");
|
||||
});
|
||||
}
|
||||
|
||||
/// 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.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 {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
@@ -15,17 +17,40 @@ class StatTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: AppColors.accentColor),
|
||||
title: Text(label, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
debugPrint("[LOG] Initialisation de StatTile pour la statistique : $label");
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
tween: Tween<double>(begin: 0.9, end: 1.0),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import '../../../../core/constants/colors.dart';
|
||||
import '../../../../domain/entities/user.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 {
|
||||
final User user;
|
||||
|
||||
@@ -10,10 +12,13 @@ class StatisticsSectionCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de StatisticsSectionCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
color: AppColors.cardColor.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -28,13 +33,73 @@ class StatisticsSectionCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
StatTile(icon: Icons.event, label: 'Événements Participés', value: '${user.eventsCount}'),
|
||||
StatTile(icon: Icons.place, label: 'Établissements Visités', value: '${user.visitedPlacesCount}'),
|
||||
StatTile(icon: Icons.post_add, label: 'Publications', value: '${user.postsCount}'),
|
||||
StatTile(icon: Icons.group, label: 'Amis/Followers', value: '${user.friendsCount}'),
|
||||
// Liste des statistiques avec animations
|
||||
_buildAnimatedStatTile(
|
||||
icon: Icons.event,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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 {
|
||||
const SupportSectionCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de SupportSectionCard.");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
color: AppColors.cardColor.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 6,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Support et Assistance',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomListTile(
|
||||
icon: Icons.help,
|
||||
label: 'Support et Assistance',
|
||||
onTap: () => print("[LOG] Accès au Support et Assistance."),
|
||||
),
|
||||
CustomListTile(
|
||||
icon: Icons.article,
|
||||
label: 'Conditions d\'utilisation',
|
||||
onTap: () => print("[LOG] Accès aux conditions d'utilisation."),
|
||||
),
|
||||
CustomListTile(
|
||||
icon: Icons.privacy_tip,
|
||||
label: 'Politique de confidentialité',
|
||||
onTap: () => print("[LOG] Accès à la politique de confidentialité."),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.help_outline,
|
||||
label: 'Support et Assistance',
|
||||
logMessage: "Accès au Support et Assistance.",
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.article_outlined,
|
||||
label: 'Conditions d\'utilisation',
|
||||
logMessage: "Accès aux conditions d'utilisation.",
|
||||
),
|
||||
_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/constants/colors.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 {
|
||||
final User user;
|
||||
|
||||
@@ -9,38 +13,64 @@ class UserInfoCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de UserInfoCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
color: AppColors.cardColor.withOpacity(0.9),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundImage: NetworkImage(user.profileImageUrl),
|
||||
backgroundColor: Colors.transparent,
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
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),
|
||||
Text(
|
||||
'${user.userFirstName} ${user.userLastName}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: AppColors.accentColor,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
user.email,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
decoration: TextDecoration.underline,
|
||||
if (!context.select((UserProvider provider) => provider.isEmailDisplayedElsewhere)) // Afficher seulement si non affiché ailleurs
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint("[LOG] Clic sur l'email de l'utilisateur : ${user.email}");
|
||||
},
|
||||
child: Text(
|
||||
user.email,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade300,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.accentColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user