import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; import '../../core/constants/env_config.dart'; import '../../core/constants/urls.dart'; import '../../data/datasources/user_remote_data_source.dart'; import '../../data/models/user_model.dart'; import '../../data/providers/friends_provider.dart'; import '../../data/services/secure_storage.dart'; import '../../domain/entities/friend.dart'; /// Dialogue pour rechercher et ajouter un nouvel ami. /// /// Ce widget permet de rechercher des utilisateurs par email ou nom, /// puis d'envoyer une demande d'ami. class AddFriendDialog extends StatefulWidget { const AddFriendDialog({ required this.onFriendAdded, super.key, }); /// Callback appelé lorsqu'un ami est ajouté avec succès final VoidCallback onFriendAdded; @override State createState() => _AddFriendDialogState(); } class _AddFriendDialogState extends State { final TextEditingController _searchController = TextEditingController(); final SecureStorage _secureStorage = SecureStorage(); late final UserRemoteDataSource _userDataSource; List _searchResults = []; bool _isSearching = false; String? _errorMessage; String? _currentUserId; @override void initState() { super.initState(); _userDataSource = UserRemoteDataSource(http.Client()); _loadCurrentUserId(); } @override void dispose() { _searchController.dispose(); super.dispose(); } /// Charge l'ID de l'utilisateur actuel Future _loadCurrentUserId() async { final userId = await _secureStorage.getUserId(); setState(() { _currentUserId = userId; }); } /// Recherche des utilisateurs par email Future _searchUsers(String query) async { if (query.trim().isEmpty) { setState(() { _searchResults = []; _errorMessage = null; }); return; } final email = query.trim().toLowerCase(); if (!_isValidEmail(email)) { setState(() { _searchResults = []; _errorMessage = 'Veuillez entrer un email valide'; _isSearching = false; }); return; } setState(() { _isSearching = true; _errorMessage = null; _searchResults = []; }); try { // Rechercher l'utilisateur par email via l'API final user = await _userDataSource.searchUserByEmail(email); setState(() { _searchResults = [user]; _isSearching = false; }); } catch (e) { if (EnvConfig.enableDetailedLogs) { debugPrint('[AddFriendDialog] Erreur de recherche: $e'); } String errorMessage; if (e.toString().contains('UserNotFoundException') || e.toString().contains('non trouvé')) { errorMessage = 'Aucun utilisateur trouvé avec cet email'; } else { errorMessage = 'Erreur lors de la recherche: ${e.toString()}'; } setState(() { _errorMessage = errorMessage; _isSearching = false; _searchResults = []; }); } } /// Vérifie si l'email est valide bool _isValidEmail(String email) { return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); } /// Ajoute un ami par son UUID Future _addFriend(String friendId) async { if (_currentUserId == null || _currentUserId!.isEmpty) { _showError('Vous devez être connecté pour ajouter un ami'); return; } if (friendId.trim().isEmpty) { _showError('ID utilisateur invalide'); return; } String? friendEmail; try { setState(() { _isSearching = true; _errorMessage = null; }); // Récupérer les informations de l'utilisateur par son ID pour affichage try { final user = await _userDataSource.getUser(friendId); friendEmail = user.email; } catch (e) { // Si l'utilisateur n'est pas trouvé, on continue quand même avec l'ID if (EnvConfig.enableDetailedLogs) { debugPrint('[AddFriendDialog] Impossible de récupérer les infos utilisateur: $e'); } } // Envoyer la demande d'ami via le provider (seulement l'UUID est nécessaire) final friendsProvider = Provider.of(context, listen: false); await friendsProvider.addFriend(friendId); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Demande d\'ami envoyée${friendEmail != null ? ' à $friendEmail' : ''}'), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green, ), ); widget.onFriendAdded(); } } catch (e) { if (EnvConfig.enableDetailedLogs) { debugPrint('[AddFriendDialog] Erreur lors de l\'ajout: $e'); debugPrint('[AddFriendDialog] Type d\'erreur: ${e.runtimeType}'); } // Extraire un message d'erreur plus clair String errorMessage; if (e.toString().contains('ValidationException')) { // Extraire le message après "ValidationException: " final parts = e.toString().split('ValidationException: '); errorMessage = parts.length > 1 ? parts[1] : 'Données invalides'; } else if (e.toString().contains('ServerException')) { final parts = e.toString().split('ServerException: '); errorMessage = parts.length > 1 ? parts[1] : 'Erreur serveur'; } else if (e.toString().contains('ConflictException')) { final parts = e.toString().split('ConflictException: '); errorMessage = parts.length > 1 ? parts[1] : 'Cet utilisateur est déjà votre ami'; } else { errorMessage = e.toString().replaceAll(RegExp(r'^[A-Za-z]+Exception: '), ''); if (errorMessage.isEmpty || errorMessage == e.toString()) { errorMessage = 'Erreur lors de l\'ajout de l\'ami. Veuillez réessayer.'; } } _showError(errorMessage); } finally { if (mounted) { setState(() { _isSearching = false; }); } } } /// Affiche une erreur void _showError(String message) { if (mounted) { setState(() { _errorMessage = message; }); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildHeader(theme), const SizedBox(height: 16), _buildSearchField(theme), const SizedBox(height: 16), if (_errorMessage != null) _buildErrorMessage(theme), if (_isSearching) _buildLoadingIndicator(theme), if (!_isSearching && _searchResults.isNotEmpty) _buildSearchResults(theme), const SizedBox(height: 16), _buildActions(theme), ], ), ), ); } /// Construit l'en-tête du dialogue Widget _buildHeader(ThemeData theme) { return Row( children: [ Icon( Icons.person_add, color: theme.colorScheme.primary, size: 28, ), const SizedBox(width: 12), Expanded( child: Text( 'Ajouter un ami', style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ); } /// Construit le champ de recherche Widget _buildSearchField(ThemeData theme) { return TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Entrez l\'email de l\'ami', prefixIcon: const Icon(Icons.person_search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); setState(() { _searchResults = []; _errorMessage = null; }); }, ) : null, helperText: 'Recherchez un utilisateur par son adresse email', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), onChanged: (value) { setState(() { _errorMessage = null; }); // Recherche automatique après un délai if (value.trim().isNotEmpty && _isValidEmail(value.trim())) { Future.delayed(const Duration(milliseconds: 500), () { if (_searchController.text == value) { _searchUsers(value.trim()); } }); } else { setState(() { _searchResults = []; }); } }, textInputAction: TextInputAction.search, keyboardType: TextInputType.emailAddress, onSubmitted: (value) { if (value.trim().isNotEmpty) { _searchUsers(value.trim()); } }, ); } /// Construit le message d'erreur Widget _buildErrorMessage(ThemeData theme) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( Icons.error_outline, color: theme.colorScheme.error, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( _errorMessage!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onErrorContainer, ), ), ), ], ), ); } /// Construit l'indicateur de chargement Widget _buildLoadingIndicator(ThemeData theme) { return const Padding( padding: EdgeInsets.all(24), child: Center( child: CircularProgressIndicator(), ), ); } /// Construit les résultats de recherche Widget _buildSearchResults(ThemeData theme) { if (_isSearching) { return const Padding( padding: EdgeInsets.all(24), child: Center( child: CircularProgressIndicator(), ), ); } if (_searchResults.isEmpty) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _searchResults.length, itemBuilder: (context, index) { final user = _searchResults[index]; return _buildUserTile(theme, user); }, ), ); } /// Construit une tuile d'utilisateur Widget _buildUserTile(ThemeData theme, UserModel user) { return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar( backgroundColor: theme.colorScheme.primaryContainer, backgroundImage: user.profileImageUrl.isNotEmpty && user.profileImageUrl.startsWith('http') ? NetworkImage(user.profileImageUrl) : null, onBackgroundImageError: (exception, stackTrace) { // Ignorer les erreurs de chargement d'image if (EnvConfig.enableDetailedLogs) { debugPrint('[AddFriendDialog] Erreur de chargement d\'image: $exception'); } }, child: user.profileImageUrl.isEmpty || !user.profileImageUrl.startsWith('http') ? Text( user.userFirstName.isNotEmpty ? user.userFirstName[0].toUpperCase() : '?', style: TextStyle( color: theme.colorScheme.onPrimaryContainer, ), ) : null, ), title: Text( '${user.userFirstName} ${user.userLastName}'.trim(), style: theme.textTheme.titleMedium, ), subtitle: Text( user.email, style: theme.textTheme.bodySmall, ), trailing: IconButton( icon: const Icon(Icons.person_add), onPressed: _isSearching ? null : () => _addFriend(user.userId), tooltip: 'Ajouter comme ami', ), ), ); } /// Construit les actions du dialogue Widget _buildActions(ThemeData theme) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ], ); } }