Refactor ActivityCard UI and improve voting functionality

- Updated ActivityCard layout for better visual consistency and responsiveness.
- Simplified the category badge and adjusted styles for better readability.
- Enhanced the voting section with a progress bar and improved button designs.
- Added a new method in Activity model to check if all trip participants approved an activity.
- Improved error handling and validation in ActivityRepository for voting and fetching activities.
- Implemented pagination in ActivityPlacesService for activity searches.
- Removed outdated scripts for cleaning up duplicate images.
This commit is contained in:
Dayron
2025-11-04 20:21:54 +01:00
parent 8ff9e12fd4
commit f6c8432335
19 changed files with 2902 additions and 961 deletions

View File

@@ -19,15 +19,14 @@ class ActivityPlacesService {
required String tripId,
ActivityCategory? category,
int radius = 5000,
int maxResults = 20,
int offset = 0,
}) async {
try {
print('ActivityPlacesService: Recherche d\'activités pour: $destination');
print('ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)');
// 1. Géocoder la destination
final coordinates = await _geocodeDestination(destination);
if (coordinates == null) {
throw Exception('Impossible de localiser la destination: $destination');
}
// 2. Rechercher les activités par catégorie ou toutes les catégories
List<Activity> allActivities = [];
@@ -67,8 +66,21 @@ class ActivityPlacesService {
final uniqueActivities = _removeDuplicates(allActivities);
uniqueActivities.sort((a, b) => (b.rating ?? 0).compareTo(a.rating ?? 0));
print('ActivityPlacesService: ${uniqueActivities.length} activités trouvées');
return uniqueActivities.take(50).toList(); // Limiter à 50 résultats
print('ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total');
// 4. Appliquer la pagination
final startIndex = offset;
final endIndex = (startIndex + maxResults).clamp(0, uniqueActivities.length);
if (startIndex >= uniqueActivities.length) {
print('ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})');
return [];
}
final paginatedResults = uniqueActivities.sublist(startIndex, endIndex);
print('ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)');
return paginatedResults;
} catch (e) {
print('ActivityPlacesService: Erreur lors de la recherche: $e');
@@ -78,29 +90,56 @@ class ActivityPlacesService {
}
/// Géocode une destination pour obtenir les coordonnées
Future<Map<String, double>?> _geocodeDestination(String destination) async {
Future<Map<String, dynamic>> _geocodeDestination(String destination) async {
try {
// Vérifier que la clé API est configurée
if (_apiKey.isEmpty) {
print('ActivityPlacesService: Clé API Google Maps manquante');
throw Exception('Clé API Google Maps non configurée');
}
final encodedDestination = Uri.encodeComponent(destination);
final url = 'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedDestination&key=$_apiKey';
print('ActivityPlacesService: Géocodage de "$destination"');
print('ActivityPlacesService: URL = $url');
final response = await http.get(Uri.parse(url));
print('ActivityPlacesService: Status code = ${response.statusCode}');
if (response.statusCode == 200) {
final data = json.decode(response.body);
print('ActivityPlacesService: Réponse géocodage = ${data['status']}');
if (data['status'] == 'OK' && data['results'].isNotEmpty) {
final location = data['results'][0]['geometry']['location'];
return {
final coordinates = {
'lat': location['lat'].toDouble(),
'lng': location['lng'].toDouble(),
};
print('ActivityPlacesService: Coordonnées trouvées = $coordinates');
return coordinates;
} else {
print('ActivityPlacesService: Erreur API = ${data['error_message'] ?? data['status']}');
if (data['status'] == 'REQUEST_DENIED') {
throw Exception('🔑 Clé API non autorisée. Activez les APIs suivantes dans Google Cloud Console:\n'
'• Geocoding API\n'
'• Places API\n'
'• Maps JavaScript API\n'
'Puis ajoutez des restrictions appropriées.');
} else if (data['status'] == 'ZERO_RESULTS') {
throw Exception('Aucun résultat trouvé pour cette destination');
} else {
throw Exception('Erreur API: ${data['status']}');
}
}
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
}
return null;
} catch (e) {
print('ActivityPlacesService: Erreur géocodage: $e');
return null;
throw e; // Rethrow pour permettre la gestion d'erreur en amont
}
}
@@ -268,9 +307,6 @@ class ActivityPlacesService {
// Géocoder la destination
final coordinates = await _geocodeDestination(destination);
if (coordinates == null) {
throw Exception('Impossible de localiser la destination');
}
final encodedQuery = Uri.encodeComponent(query);
final url = 'https://maps.googleapis.com/maps/api/place/textsearch/json'
@@ -338,4 +374,190 @@ class ActivityPlacesService {
return ActivityCategory.attraction; // Par défaut
}
/// Recherche d'activités avec pagination (6 par page)
Future<Map<String, dynamic>> searchActivitiesPaginated({
required String destination,
required String tripId,
ActivityCategory? category,
int pageSize = 6,
String? nextPageToken,
int radius = 5000,
}) async {
try {
print('ActivityPlacesService: Recherche paginée pour: $destination (page: ${nextPageToken ?? "première"})');
// 1. Géocoder la destination
final coordinates = await _geocodeDestination(destination);
// 2. Rechercher les activités par catégorie avec pagination
if (category != null) {
return await _searchByCategoryPaginated(
coordinates['lat']!,
coordinates['lng']!,
category,
tripId,
radius,
pageSize,
nextPageToken,
);
} else {
// Pour toutes les catégories, faire une recherche générale paginée
return await _searchAllCategoriesPaginated(
coordinates['lat']!,
coordinates['lng']!,
tripId,
radius,
pageSize,
nextPageToken,
);
}
} catch (e) {
print('ActivityPlacesService: Erreur recherche paginée: $e');
_errorService.logError('activity_places_service', e);
return {
'activities': <Activity>[],
'nextPageToken': null,
'hasMoreData': false,
};
}
}
/// Recherche paginée par catégorie spécifique
Future<Map<String, dynamic>> _searchByCategoryPaginated(
double lat,
double lng,
ActivityCategory category,
String tripId,
int radius,
int pageSize,
String? nextPageToken,
) async {
try {
String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'?location=$lat,$lng'
'&radius=$radius'
'&type=${category.googlePlaceType}'
'&key=$_apiKey';
if (nextPageToken != null) {
url += '&pagetoken=$nextPageToken';
}
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final List<Activity> activities = [];
final results = data['results'] as List? ?? [];
// Limiter à pageSize résultats
final limitedResults = results.take(pageSize).toList();
for (final place in limitedResults) {
try {
final activity = await _convertPlaceToActivity(place, tripId, category);
if (activity != null) {
activities.add(activity);
}
} catch (e) {
print('ActivityPlacesService: Erreur conversion place: $e');
}
}
return {
'activities': activities,
'nextPageToken': data['next_page_token'],
'hasMoreData': data['next_page_token'] != null,
};
}
}
return {
'activities': <Activity>[],
'nextPageToken': null,
'hasMoreData': false,
};
} catch (e) {
print('ActivityPlacesService: Erreur recherche catégorie paginée: $e');
return {
'activities': <Activity>[],
'nextPageToken': null,
'hasMoreData': false,
};
}
}
/// Recherche paginée pour toutes les catégories
Future<Map<String, dynamic>> _searchAllCategoriesPaginated(
double lat,
double lng,
String tripId,
int radius,
int pageSize,
String? nextPageToken,
) async {
try {
// Pour toutes les catégories, on utilise une recherche plus générale
String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'?location=$lat,$lng'
'&radius=$radius'
'&type=tourist_attraction'
'&key=$_apiKey';
if (nextPageToken != null) {
url += '&pagetoken=$nextPageToken';
}
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final List<Activity> activities = [];
final results = data['results'] as List? ?? [];
// Limiter à pageSize résultats
final limitedResults = results.take(pageSize).toList();
for (final place in limitedResults) {
try {
// Déterminer la catégorie basée sur les types du lieu
final types = List<String>.from(place['types'] ?? []);
final category = _determineCategoryFromTypes(types);
final activity = await _convertPlaceToActivity(place, tripId, category);
if (activity != null) {
activities.add(activity);
}
} catch (e) {
print('ActivityPlacesService: Erreur conversion place: $e');
}
}
return {
'activities': activities,
'nextPageToken': data['next_page_token'],
'hasMoreData': data['next_page_token'] != null,
};
}
}
return {
'activities': <Activity>[],
'nextPageToken': null,
'hasMoreData': false,
};
} catch (e) {
print('ActivityPlacesService: Erreur recherche toutes catégories paginée: $e');
return {
'activities': <Activity>[],
'nextPageToken': null,
'hasMoreData': false,
};
}
}
}