feat: Add calendar page, enhance activity search and approval logic, and refactor activity filtering UI.
This commit is contained in:
@@ -21,9 +21,10 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
_placesService = placesService,
|
_placesService = placesService,
|
||||||
_errorService = errorService,
|
_errorService = errorService,
|
||||||
super(const ActivityInitial()) {
|
super(const ActivityInitial()) {
|
||||||
|
|
||||||
on<LoadActivities>(_onLoadActivities);
|
on<LoadActivities>(_onLoadActivities);
|
||||||
on<LoadTripActivitiesPreservingSearch>(_onLoadTripActivitiesPreservingSearch);
|
on<LoadTripActivitiesPreservingSearch>(
|
||||||
|
_onLoadTripActivitiesPreservingSearch,
|
||||||
|
);
|
||||||
on<SearchActivities>(_onSearchActivities);
|
on<SearchActivities>(_onSearchActivities);
|
||||||
on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates);
|
on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates);
|
||||||
on<SearchActivitiesByText>(_onSearchActivitiesByText);
|
on<SearchActivitiesByText>(_onSearchActivitiesByText);
|
||||||
@@ -39,6 +40,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
on<RestoreCachedSearchResults>(_onRestoreCachedSearchResults);
|
on<RestoreCachedSearchResults>(_onRestoreCachedSearchResults);
|
||||||
on<RemoveFromSearchResults>(_onRemoveFromSearchResults);
|
on<RemoveFromSearchResults>(_onRemoveFromSearchResults);
|
||||||
on<AddActivityAndRemoveFromSearch>(_onAddActivityAndRemoveFromSearch);
|
on<AddActivityAndRemoveFromSearch>(_onAddActivityAndRemoveFromSearch);
|
||||||
|
on<UpdateActivityDate>(_onUpdateActivityDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles loading activities for a trip
|
/// Handles loading activities for a trip
|
||||||
@@ -51,12 +53,14 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
|
|
||||||
final activities = await _repository.getActivitiesByTrip(event.tripId);
|
final activities = await _repository.getActivitiesByTrip(event.tripId);
|
||||||
|
|
||||||
emit(ActivityLoaded(
|
emit(
|
||||||
activities: activities,
|
ActivityLoaded(activities: activities, filteredActivities: activities),
|
||||||
filteredActivities: activities,
|
);
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError('activity_bloc', 'Erreur chargement activités: $e');
|
_errorService.logError(
|
||||||
|
'activity_bloc',
|
||||||
|
'Erreur chargement activités: $e',
|
||||||
|
);
|
||||||
emit(const ActivityError('Impossible de charger les activités'));
|
emit(const ActivityError('Impossible de charger les activités'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,16 +80,44 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sinon, on charge normalement
|
// Sinon, on charge normalement
|
||||||
emit(ActivityLoaded(
|
emit(
|
||||||
activities: activities,
|
ActivityLoaded(activities: activities, filteredActivities: activities),
|
||||||
filteredActivities: activities,
|
);
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError('activity_bloc', 'Erreur chargement activités: $e');
|
_errorService.logError(
|
||||||
|
'activity_bloc',
|
||||||
|
'Erreur chargement activités: $e',
|
||||||
|
);
|
||||||
emit(const ActivityError('Impossible de charger les activités'));
|
emit(const ActivityError('Impossible de charger les activités'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpdateActivityDate(
|
||||||
|
UpdateActivityDate event,
|
||||||
|
Emitter<ActivityState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final activity = await _repository.getActivity(
|
||||||
|
event.tripId,
|
||||||
|
event.activityId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activity != null) {
|
||||||
|
final updatedActivity = activity.copyWith(
|
||||||
|
date: event.date,
|
||||||
|
clearDate: event.date == null,
|
||||||
|
);
|
||||||
|
await _repository.updateActivity(updatedActivity);
|
||||||
|
|
||||||
|
// Recharger les activités pour mettre à jour l'UI
|
||||||
|
add(LoadActivities(event.tripId));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_errorService.logError('activity_bloc', 'Erreur mise à jour date: $e');
|
||||||
|
emit(const ActivityError('Impossible de mettre à jour la date'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles searching activities using Google Places API
|
/// Handles searching activities using Google Places API
|
||||||
Future<void> _onSearchActivities(
|
Future<void> _onSearchActivities(
|
||||||
SearchActivities event,
|
SearchActivities event,
|
||||||
@@ -104,7 +136,9 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
destination: event.destination,
|
destination: event.destination,
|
||||||
tripId: event.tripId,
|
tripId: event.tripId,
|
||||||
category: event.category,
|
category: event.category,
|
||||||
maxResults: event.maxResults ?? 20, // Par défaut 20, ou utiliser la valeur spécifiée
|
maxResults:
|
||||||
|
event.maxResults ??
|
||||||
|
20, // Par défaut 20, ou utiliser la valeur spécifiée
|
||||||
offset: event.offset ?? 0, // Par défaut 0
|
offset: event.offset ?? 0, // Par défaut 0
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -121,11 +155,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
// Mettre en cache les résultats
|
// Mettre en cache les résultats
|
||||||
ActivityCacheService().setCachedActivities(event.tripId, finalResults);
|
ActivityCacheService().setCachedActivities(event.tripId, finalResults);
|
||||||
|
|
||||||
emit(ActivitySearchResults(
|
emit(
|
||||||
|
ActivitySearchResults(
|
||||||
searchResults: finalResults,
|
searchResults: finalResults,
|
||||||
query: event.category?.displayName ?? 'Toutes les activités',
|
query: event.category?.displayName ?? 'Toutes les activités',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError('activity_bloc', 'Erreur recherche activités: $e');
|
_errorService.logError('activity_bloc', 'Erreur recherche activités: $e');
|
||||||
emit(const ActivityError('Impossible de rechercher les activités'));
|
emit(const ActivityError('Impossible de rechercher les activités'));
|
||||||
@@ -168,13 +204,18 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
// Mettre en cache les résultats
|
// Mettre en cache les résultats
|
||||||
ActivityCacheService().setCachedActivities(event.tripId, finalResults);
|
ActivityCacheService().setCachedActivities(event.tripId, finalResults);
|
||||||
|
|
||||||
emit(ActivitySearchResults(
|
emit(
|
||||||
|
ActivitySearchResults(
|
||||||
searchResults: finalResults,
|
searchResults: finalResults,
|
||||||
query: event.category?.displayName ?? 'Toutes les activités',
|
query: event.category?.displayName ?? 'Toutes les activités',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError('activity_bloc', 'Erreur recherche activités avec coordonnées: $e');
|
_errorService.logError(
|
||||||
|
'activity_bloc',
|
||||||
|
'Erreur recherche activités avec coordonnées: $e',
|
||||||
|
);
|
||||||
emit(const ActivityError('Impossible de rechercher les activités'));
|
emit(const ActivityError('Impossible de rechercher les activités'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,10 +237,9 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
// Mettre en cache les résultats
|
// Mettre en cache les résultats
|
||||||
ActivityCacheService().setCachedActivities(event.tripId, searchResults);
|
ActivityCacheService().setCachedActivities(event.tripId, searchResults);
|
||||||
|
|
||||||
emit(ActivitySearchResults(
|
emit(
|
||||||
searchResults: searchResults,
|
ActivitySearchResults(searchResults: searchResults, query: event.query),
|
||||||
query: event.query,
|
);
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError('activity_bloc', 'Erreur recherche textuelle: $e');
|
_errorService.logError('activity_bloc', 'Erreur recherche textuelle: $e');
|
||||||
emit(const ActivityError('Impossible de rechercher les activités'));
|
emit(const ActivityError('Impossible de rechercher les activités'));
|
||||||
@@ -230,16 +270,23 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
if (activityId != null) {
|
if (activityId != null) {
|
||||||
// Si on est en état de recherche (suggestions Google), préserver cet état
|
// Si on est en état de recherche (suggestions Google), préserver cet état
|
||||||
if (state is ActivitySearchResults) {
|
if (state is ActivitySearchResults) {
|
||||||
// On ne change rien à l'état de recherche, on le garde tel quel
|
final currentState = state as ActivitySearchResults;
|
||||||
// La suppression de l'activité des résultats se fait dans _onRemoveFromSearchResults
|
// On garde l'état de recherche inchangé mais on ajoute l'info de l'activité ajoutée
|
||||||
|
emit(
|
||||||
|
currentState.copyWith(
|
||||||
|
newlyAddedActivity: event.activity.copyWith(id: activityId),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sinon, émettre l'état d'ajout réussi
|
// Sinon, émettre l'état d'ajout réussi
|
||||||
emit(ActivityAdded(
|
emit(
|
||||||
|
ActivityAdded(
|
||||||
activity: event.activity.copyWith(id: activityId),
|
activity: event.activity.copyWith(id: activityId),
|
||||||
message: 'Activité ajoutée avec succès',
|
message: 'Activité ajoutée avec succès',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
// Reload activities while preserving search results
|
// Reload activities while preserving search results
|
||||||
add(LoadTripActivitiesPreservingSearch(event.activity.tripId));
|
add(LoadTripActivitiesPreservingSearch(event.activity.tripId));
|
||||||
} else {
|
} else {
|
||||||
@@ -281,19 +328,23 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
.where((activity) => activity.id != event.googleActivityId)
|
.where((activity) => activity.id != event.googleActivityId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
emit(ActivitySearchResults(
|
emit(
|
||||||
|
ActivitySearchResults(
|
||||||
searchResults: updatedResults,
|
searchResults: updatedResults,
|
||||||
query: currentState.query,
|
query: currentState.query,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sinon, émettre l'état d'ajout réussi
|
// Sinon, émettre l'état d'ajout réussi
|
||||||
emit(ActivityAdded(
|
emit(
|
||||||
|
ActivityAdded(
|
||||||
activity: event.activity.copyWith(id: activityId),
|
activity: event.activity.copyWith(id: activityId),
|
||||||
message: 'Activité ajoutée avec succès',
|
message: 'Activité ajoutée avec succès',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
// Reload activities while preserving search results
|
// Reload activities while preserving search results
|
||||||
add(LoadTripActivitiesPreservingSearch(event.activity.tripId));
|
add(LoadTripActivitiesPreservingSearch(event.activity.tripId));
|
||||||
} else {
|
} else {
|
||||||
@@ -314,11 +365,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
// Filter out existing activities
|
// Filter out existing activities
|
||||||
final filteredActivities = <Activity>[];
|
final filteredActivities = <Activity>[];
|
||||||
|
|
||||||
emit(ActivityBatchAdding(
|
emit(
|
||||||
|
ActivityBatchAdding(
|
||||||
activitiesToAdd: event.activities,
|
activitiesToAdd: event.activities,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
total: event.activities.length,
|
total: event.activities.length,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
for (int i = 0; i < event.activities.length; i++) {
|
for (int i = 0; i < event.activities.length; i++) {
|
||||||
final activity = event.activities[i];
|
final activity = event.activities[i];
|
||||||
@@ -337,11 +390,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update progress
|
// Update progress
|
||||||
emit(ActivityBatchAdding(
|
emit(
|
||||||
|
ActivityBatchAdding(
|
||||||
activitiesToAdd: event.activities,
|
activitiesToAdd: event.activities,
|
||||||
progress: i + 1,
|
progress: i + 1,
|
||||||
total: event.activities.length,
|
total: event.activities.length,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredActivities.isEmpty) {
|
if (filteredActivities.isEmpty) {
|
||||||
@@ -352,10 +407,12 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
final addedIds = await _repository.addActivitiesBatch(filteredActivities);
|
final addedIds = await _repository.addActivitiesBatch(filteredActivities);
|
||||||
|
|
||||||
if (addedIds.isNotEmpty) {
|
if (addedIds.isNotEmpty) {
|
||||||
emit(ActivityOperationSuccess(
|
emit(
|
||||||
|
ActivityOperationSuccess(
|
||||||
'${addedIds.length} activité(s) ajoutée(s) avec succès',
|
'${addedIds.length} activité(s) ajoutée(s) avec succès',
|
||||||
operationType: 'batch_add',
|
operationType: 'batch_add',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
// Reload activities
|
// Reload activities
|
||||||
add(LoadActivities(event.activities.first.tripId));
|
add(LoadActivities(event.activities.first.tripId));
|
||||||
} else {
|
} else {
|
||||||
@@ -376,10 +433,12 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
// Show voting state
|
// Show voting state
|
||||||
if (state is ActivityLoaded) {
|
if (state is ActivityLoaded) {
|
||||||
final currentState = state as ActivityLoaded;
|
final currentState = state as ActivityLoaded;
|
||||||
emit(ActivityVoting(
|
emit(
|
||||||
|
ActivityVoting(
|
||||||
activityId: event.activityId,
|
activityId: event.activityId,
|
||||||
activities: currentState.activities,
|
activities: currentState.activities,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final success = await _repository.voteForActivity(
|
final success = await _repository.voteForActivity(
|
||||||
@@ -389,11 +448,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
emit(ActivityVoteRecorded(
|
emit(
|
||||||
|
ActivityVoteRecorded(
|
||||||
activityId: event.activityId,
|
activityId: event.activityId,
|
||||||
vote: event.vote,
|
vote: event.vote,
|
||||||
userId: event.userId,
|
userId: event.userId,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Reload activities to reflect the new vote
|
// Reload activities to reflect the new vote
|
||||||
if (state is ActivityLoaded) {
|
if (state is ActivityLoaded) {
|
||||||
@@ -402,7 +463,8 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
currentState.activities.first.tripId,
|
currentState.activities.first.tripId,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(currentState.copyWith(
|
emit(
|
||||||
|
currentState.copyWith(
|
||||||
activities: activities,
|
activities: activities,
|
||||||
filteredActivities: _applyFilters(
|
filteredActivities: _applyFilters(
|
||||||
activities,
|
activities,
|
||||||
@@ -411,7 +473,8 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
currentState.showVotedOnly,
|
currentState.showVotedOnly,
|
||||||
event.userId,
|
event.userId,
|
||||||
),
|
),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emit(const ActivityError('Impossible d\'enregistrer le vote'));
|
emit(const ActivityError('Impossible d\'enregistrer le vote'));
|
||||||
@@ -431,10 +494,12 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
final success = await _repository.deleteActivity(event.activityId);
|
final success = await _repository.deleteActivity(event.activityId);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
emit(ActivityDeleted(
|
emit(
|
||||||
|
ActivityDeleted(
|
||||||
activityId: event.activityId,
|
activityId: event.activityId,
|
||||||
message: 'Activité supprimée avec succès',
|
message: 'Activité supprimée avec succès',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Reload if we're on the activity list
|
// Reload if we're on the activity list
|
||||||
if (state is ActivityLoaded) {
|
if (state is ActivityLoaded) {
|
||||||
@@ -468,12 +533,14 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
'', // UserId would be needed for showVotedOnly filter
|
'', // UserId would be needed for showVotedOnly filter
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(currentState.copyWith(
|
emit(
|
||||||
|
currentState.copyWith(
|
||||||
filteredActivities: filteredActivities,
|
filteredActivities: filteredActivities,
|
||||||
activeFilter: event.category,
|
activeFilter: event.category,
|
||||||
minRating: event.minRating,
|
minRating: event.minRating,
|
||||||
showVotedOnly: event.showVotedOnly ?? false,
|
showVotedOnly: event.showVotedOnly ?? false,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,19 +570,23 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
try {
|
try {
|
||||||
if (state is ActivityLoaded) {
|
if (state is ActivityLoaded) {
|
||||||
final currentState = state as ActivityLoaded;
|
final currentState = state as ActivityLoaded;
|
||||||
emit(ActivityUpdating(
|
emit(
|
||||||
|
ActivityUpdating(
|
||||||
activityId: event.activity.id,
|
activityId: event.activity.id,
|
||||||
activities: currentState.activities,
|
activities: currentState.activities,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final success = await _repository.updateActivity(event.activity);
|
final success = await _repository.updateActivity(event.activity);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
emit(const ActivityOperationSuccess(
|
emit(
|
||||||
|
const ActivityOperationSuccess(
|
||||||
'Activité mise à jour avec succès',
|
'Activité mise à jour avec succès',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Reload activities
|
// Reload activities
|
||||||
add(LoadActivities(event.activity.tripId));
|
add(LoadActivities(event.activity.tripId));
|
||||||
@@ -536,11 +607,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
try {
|
try {
|
||||||
// This would require extending the Activity model to include favorites
|
// This would require extending the Activity model to include favorites
|
||||||
// For now, we'll use the voting system as a favorite system
|
// For now, we'll use the voting system as a favorite system
|
||||||
add(VoteForActivity(
|
add(
|
||||||
|
VoteForActivity(
|
||||||
activityId: event.activityId,
|
activityId: event.activityId,
|
||||||
userId: event.userId,
|
userId: event.userId,
|
||||||
vote: 1,
|
vote: 1,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError('activity_bloc', 'Erreur favori: $e');
|
_errorService.logError('activity_bloc', 'Erreur favori: $e');
|
||||||
emit(const ActivityError('Impossible de modifier les favoris'));
|
emit(const ActivityError('Impossible de modifier les favoris'));
|
||||||
@@ -558,7 +631,25 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
var filtered = activities;
|
var filtered = activities;
|
||||||
|
|
||||||
if (category != null) {
|
if (category != null) {
|
||||||
filtered = filtered.where((a) => a.category == category).toList();
|
filtered = filtered.where((a) {
|
||||||
|
// Check exact match (internal value)
|
||||||
|
if (a.category == category) return true;
|
||||||
|
|
||||||
|
// Check display name match
|
||||||
|
// Find the enum that matches the filter category (which is a display name)
|
||||||
|
try {
|
||||||
|
final categoryEnum = ActivityCategory.values.firstWhere(
|
||||||
|
(e) => e.displayName == category,
|
||||||
|
);
|
||||||
|
// Check if activity category matches the enum's google type or display name
|
||||||
|
return a.category == categoryEnum.googlePlaceType ||
|
||||||
|
a.category == categoryEnum.displayName ||
|
||||||
|
a.category.toLowerCase() == categoryEnum.name.toLowerCase();
|
||||||
|
} catch (_) {
|
||||||
|
// If no matching enum found, fallback to simple string comparison
|
||||||
|
return a.category.toLowerCase() == category.toLowerCase();
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minRating != null) {
|
if (minRating != null) {
|
||||||
@@ -587,11 +678,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Émettre le nouvel état avec l'activité retirée
|
// Émettre le nouvel état avec l'activité retirée
|
||||||
emit(ActivitySearchResults(
|
emit(
|
||||||
|
ActivitySearchResults(
|
||||||
searchResults: updatedResults,
|
searchResults: updatedResults,
|
||||||
query: currentState.query,
|
query: currentState.query,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,10 +693,12 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
|||||||
RestoreCachedSearchResults event,
|
RestoreCachedSearchResults event,
|
||||||
Emitter<ActivityState> emit,
|
Emitter<ActivityState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(ActivitySearchResults(
|
emit(
|
||||||
|
ActivitySearchResults(
|
||||||
searchResults: event.searchResults,
|
searchResults: event.searchResults,
|
||||||
query: 'cached',
|
query: 'cached',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,15 @@ class SearchActivities extends ActivityEvent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [tripId, destination, category, maxResults, offset, reset, appendToExisting];
|
List<Object?> get props => [
|
||||||
|
tripId,
|
||||||
|
destination,
|
||||||
|
category,
|
||||||
|
maxResults,
|
||||||
|
offset,
|
||||||
|
reset,
|
||||||
|
appendToExisting,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event to search activities using coordinates directly (bypasses geocoding)
|
/// Event to search activities using coordinates directly (bypasses geocoding)
|
||||||
@@ -76,7 +84,16 @@ class SearchActivitiesWithCoordinates extends ActivityEvent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [tripId, latitude, longitude, category, maxResults, offset, reset, appendToExisting];
|
List<Object?> get props => [
|
||||||
|
tripId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
category,
|
||||||
|
maxResults,
|
||||||
|
offset,
|
||||||
|
reset,
|
||||||
|
appendToExisting,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event to search activities by text query
|
/// Event to search activities by text query
|
||||||
@@ -95,6 +112,21 @@ class SearchActivitiesByText extends ActivityEvent {
|
|||||||
List<Object> get props => [tripId, destination, query];
|
List<Object> get props => [tripId, destination, query];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UpdateActivityDate extends ActivityEvent {
|
||||||
|
final String tripId;
|
||||||
|
final String activityId;
|
||||||
|
final DateTime? date;
|
||||||
|
|
||||||
|
const UpdateActivityDate({
|
||||||
|
required this.tripId,
|
||||||
|
required this.activityId,
|
||||||
|
this.date,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tripId, activityId, date];
|
||||||
|
}
|
||||||
|
|
||||||
/// Event to add a single activity to the trip
|
/// Event to add a single activity to the trip
|
||||||
class AddActivity extends ActivityEvent {
|
class AddActivity extends ActivityEvent {
|
||||||
final Activity activity;
|
final Activity activity;
|
||||||
@@ -147,11 +179,7 @@ class FilterActivities extends ActivityEvent {
|
|||||||
final double? minRating;
|
final double? minRating;
|
||||||
final bool? showVotedOnly;
|
final bool? showVotedOnly;
|
||||||
|
|
||||||
const FilterActivities({
|
const FilterActivities({this.category, this.minRating, this.showVotedOnly});
|
||||||
this.category,
|
|
||||||
this.minRating,
|
|
||||||
this.showVotedOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [category, minRating, showVotedOnly];
|
List<Object?> get props => [category, minRating, showVotedOnly];
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ class ActivityLoaded extends ActivityState {
|
|||||||
|
|
||||||
/// Gets activities by category
|
/// Gets activities by category
|
||||||
List<Activity> getActivitiesByCategory(String category) {
|
List<Activity> getActivitiesByCategory(String category) {
|
||||||
return activities.where((activity) => activity.category == category).toList();
|
return activities
|
||||||
|
.where((activity) => activity.category == category)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets top rated activities
|
/// Gets top rated activities
|
||||||
@@ -94,26 +96,35 @@ class ActivitySearchResults extends ActivityState {
|
|||||||
final List<Activity> searchResults;
|
final List<Activity> searchResults;
|
||||||
final String query;
|
final String query;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
final Activity? newlyAddedActivity;
|
||||||
|
|
||||||
const ActivitySearchResults({
|
const ActivitySearchResults({
|
||||||
required this.searchResults,
|
required this.searchResults,
|
||||||
required this.query,
|
required this.query,
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
|
this.newlyAddedActivity,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [searchResults, query, isLoading];
|
List<Object?> get props => [
|
||||||
|
searchResults,
|
||||||
|
query,
|
||||||
|
isLoading,
|
||||||
|
newlyAddedActivity,
|
||||||
|
];
|
||||||
|
|
||||||
/// Creates a copy with optional modifications
|
/// Creates a copy with optional modifications
|
||||||
ActivitySearchResults copyWith({
|
ActivitySearchResults copyWith({
|
||||||
List<Activity>? searchResults,
|
List<Activity>? searchResults,
|
||||||
String? query,
|
String? query,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
|
Activity? newlyAddedActivity,
|
||||||
}) {
|
}) {
|
||||||
return ActivitySearchResults(
|
return ActivitySearchResults(
|
||||||
searchResults: searchResults ?? this.searchResults,
|
searchResults: searchResults ?? this.searchResults,
|
||||||
query: query ?? this.query,
|
query: query ?? this.query,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
newlyAddedActivity: newlyAddedActivity ?? this.newlyAddedActivity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,10 +134,7 @@ class ActivityOperationSuccess extends ActivityState {
|
|||||||
final String message;
|
final String message;
|
||||||
final String? operationType;
|
final String? operationType;
|
||||||
|
|
||||||
const ActivityOperationSuccess(
|
const ActivityOperationSuccess(this.message, {this.operationType});
|
||||||
this.message, {
|
|
||||||
this.operationType,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [message, operationType];
|
List<Object?> get props => [message, operationType];
|
||||||
@@ -138,11 +146,7 @@ class ActivityError extends ActivityState {
|
|||||||
final String? errorCode;
|
final String? errorCode;
|
||||||
final dynamic error;
|
final dynamic error;
|
||||||
|
|
||||||
const ActivityError(
|
const ActivityError(this.message, {this.errorCode, this.error});
|
||||||
this.message, {
|
|
||||||
this.errorCode,
|
|
||||||
this.error,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [message, errorCode, error];
|
List<Object?> get props => [message, errorCode, error];
|
||||||
@@ -153,10 +157,7 @@ class ActivityVoting extends ActivityState {
|
|||||||
final String activityId;
|
final String activityId;
|
||||||
final List<Activity> activities;
|
final List<Activity> activities;
|
||||||
|
|
||||||
const ActivityVoting({
|
const ActivityVoting({required this.activityId, required this.activities});
|
||||||
required this.activityId,
|
|
||||||
required this.activities,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [activityId, activities];
|
List<Object> get props => [activityId, activities];
|
||||||
@@ -167,10 +168,7 @@ class ActivityUpdating extends ActivityState {
|
|||||||
final String activityId;
|
final String activityId;
|
||||||
final List<Activity> activities;
|
final List<Activity> activities;
|
||||||
|
|
||||||
const ActivityUpdating({
|
const ActivityUpdating({required this.activityId, required this.activities});
|
||||||
required this.activityId,
|
|
||||||
required this.activities,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [activityId, activities];
|
List<Object> get props => [activityId, activities];
|
||||||
@@ -200,10 +198,7 @@ class ActivityAdded extends ActivityState {
|
|||||||
final Activity activity;
|
final Activity activity;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
const ActivityAdded({
|
const ActivityAdded({required this.activity, required this.message});
|
||||||
required this.activity,
|
|
||||||
required this.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [activity, message];
|
List<Object> get props => [activity, message];
|
||||||
@@ -214,10 +209,7 @@ class ActivityDeleted extends ActivityState {
|
|||||||
final String activityId;
|
final String activityId;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
const ActivityDeleted({
|
const ActivityDeleted({required this.activityId, required this.message});
|
||||||
required this.activityId,
|
|
||||||
required this.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [activityId, message];
|
List<Object> get props => [activityId, message];
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import '../../blocs/activity/activity_state.dart';
|
|||||||
import '../../models/trip.dart';
|
import '../../models/trip.dart';
|
||||||
import '../../models/activity.dart';
|
import '../../models/activity.dart';
|
||||||
import '../../services/activity_cache_service.dart';
|
import '../../services/activity_cache_service.dart';
|
||||||
import '../activities/add_activity_bottom_sheet.dart';
|
|
||||||
import '../loading/laoding_content.dart';
|
import '../loading/laoding_content.dart';
|
||||||
|
|
||||||
class ActivitiesPage extends StatefulWidget {
|
class ActivitiesPage extends StatefulWidget {
|
||||||
@@ -23,9 +23,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final ActivityCacheService _cacheService = ActivityCacheService();
|
final ActivityCacheService _cacheService = ActivityCacheService();
|
||||||
String _selectedCategory = 'Toutes les catégories';
|
|
||||||
String _selectedPrice = 'Prix';
|
|
||||||
String _selectedRating = 'Note';
|
|
||||||
|
|
||||||
// Cache pour éviter de recharger les données
|
// Cache pour éviter de recharger les données
|
||||||
bool _activitiesLoaded = false;
|
bool _activitiesLoaded = false;
|
||||||
@@ -83,7 +80,9 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
// Si on va sur l'onglet suggestions Google et qu'aucune recherche n'a été faite
|
// Si on va sur l'onglet suggestions Google et qu'aucune recherche n'a été faite
|
||||||
if (_tabController.index == 2 && !_googleSearchPerformed) {
|
if (_tabController.index == 2 && !_googleSearchPerformed) {
|
||||||
// Vérifier si on a des activités en cache
|
// Vérifier si on a des activités en cache
|
||||||
final cachedActivities = _cacheService.getCachedActivities(widget.trip.id!);
|
final cachedActivities = _cacheService.getCachedActivities(
|
||||||
|
widget.trip.id!,
|
||||||
|
);
|
||||||
if (cachedActivities != null && cachedActivities.isNotEmpty) {
|
if (cachedActivities != null && cachedActivities.isNotEmpty) {
|
||||||
// Restaurer les activités en cache dans le BLoC
|
// Restaurer les activités en cache dans le BLoC
|
||||||
context.read<ActivityBloc>().add(
|
context.read<ActivityBloc>().add(
|
||||||
@@ -174,10 +173,26 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
// Stocker les activités localement
|
// Stocker les activités localement
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tripActivities = state.activities;
|
final allActivities = state.filteredActivities;
|
||||||
_approvedActivities = state.activities
|
|
||||||
.where((a) => a.totalVotes > 0)
|
_approvedActivities = allActivities
|
||||||
|
.where(
|
||||||
|
(a) => a.isApprovedByAllParticipants([
|
||||||
|
...widget.trip.participants,
|
||||||
|
widget.trip.createdBy,
|
||||||
|
]),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
_tripActivities = allActivities
|
||||||
|
.where(
|
||||||
|
(a) => !a.isApprovedByAllParticipants([
|
||||||
|
...widget.trip.participants,
|
||||||
|
widget.trip.createdBy,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
_isLoadingTripActivities = false;
|
_isLoadingTripActivities = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,6 +204,37 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state is ActivitySearchResults) {
|
if (state is ActivitySearchResults) {
|
||||||
|
// Gérer l'ajout d'activité depuis les résultats de recherche
|
||||||
|
if (state.newlyAddedActivity != null) {
|
||||||
|
// Fermer le dialog de loading s'il est ouvert
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
_tripActivities.add(state.newlyAddedActivity!);
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'${state.newlyAddedActivity!.name} ajoutée au voyage !',
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'Voir',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
_tabController.animateTo(0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats)
|
// Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats)
|
||||||
// et pas pour les rechargements automatiques
|
// et pas pour les rechargements automatiques
|
||||||
if (state.searchResults.length <= 6 && !_autoReloadInProgress) {
|
if (state.searchResults.length <= 6 && !_autoReloadInProgress) {
|
||||||
@@ -220,21 +266,13 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
backgroundColor: theme.colorScheme.surface,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
foregroundColor: theme.colorScheme.onSurface,
|
foregroundColor: theme.colorScheme.onSurface,
|
||||||
actions: [
|
actions: [],
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
onPressed: _showAddActivityBottomSheet,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Barre de recherche
|
// Barre de recherche
|
||||||
_buildSearchBar(theme),
|
_buildSearchBar(theme),
|
||||||
|
|
||||||
// Filtres
|
|
||||||
_buildFilters(theme),
|
|
||||||
|
|
||||||
// Onglets de catégories
|
// Onglets de catégories
|
||||||
_buildCategoryTabs(theme),
|
_buildCategoryTabs(theme),
|
||||||
|
|
||||||
@@ -260,7 +298,9 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||||
|
alpha: 0.3,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
@@ -280,95 +320,32 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onSubmitted: (value) {
|
||||||
// TODO: Implémenter la recherche
|
if (value.isNotEmpty) {
|
||||||
|
_performSearch(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilters(ThemeData theme) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildFilterButton(
|
|
||||||
theme,
|
|
||||||
_selectedCategory,
|
|
||||||
Icons.category,
|
|
||||||
() => _showCategoryFilter(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildFilterButton(
|
|
||||||
theme,
|
|
||||||
_selectedPrice,
|
|
||||||
Icons.euro,
|
|
||||||
() => _showPriceFilter(),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildFilterButton(
|
|
||||||
theme,
|
|
||||||
_selectedRating,
|
|
||||||
Icons.star,
|
|
||||||
() => _showRatingFilter(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFilterButton(
|
|
||||||
ThemeData theme,
|
|
||||||
String text,
|
|
||||||
IconData icon,
|
|
||||||
VoidCallback onPressed,
|
|
||||||
) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: onPressed,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline.withValues(alpha:0.5)),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
color: theme.colorScheme.surface,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha:0.7),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha:0.7),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCategoryTabs(ThemeData theme) {
|
Widget _buildCategoryTabs(ThemeData theme) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3),
|
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||||
|
alpha: 0.3,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
labelColor: Colors.white,
|
labelColor: Colors.white,
|
||||||
unselectedLabelColor: theme.colorScheme.onSurface.withValues(alpha:0.7),
|
unselectedLabelColor: theme.colorScheme.onSurface.withValues(
|
||||||
|
alpha: 0.7,
|
||||||
|
),
|
||||||
indicator: BoxDecoration(
|
indicator: BoxDecoration(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
@@ -376,246 +353,15 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
indicatorSize: TabBarIndicatorSize.tab,
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
dividerColor: Colors.transparent,
|
dividerColor: Colors.transparent,
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(text: 'Activités du voyage'),
|
Tab(text: 'Voyage'),
|
||||||
Tab(text: 'Activités approuvées'),
|
Tab(text: 'Approuvées'),
|
||||||
Tab(text: 'Suggestions Google'),
|
Tab(text: 'Suggestion'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCategoryFilter() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => _buildCategoryFilterSheet(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showPriceFilter() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => _buildPriceFilterSheet(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showRatingFilter() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => _buildRatingFilterSheet(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCategoryFilterSheet() {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final categories = [
|
|
||||||
'Toutes les catégories',
|
|
||||||
...ActivityCategory.values.map((e) => e.displayName),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight:
|
|
||||||
MediaQuery.of(context).size.height * 0.7, // Limite à 70% de l'écran
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Catégories',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: categories
|
|
||||||
.map(
|
|
||||||
(category) => ListTile(
|
|
||||||
title: Text(category),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedCategory = category;
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
trailing: _selectedCategory == category
|
|
||||||
? Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPriceFilterSheet() {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final prices = [
|
|
||||||
'Prix',
|
|
||||||
'Gratuit',
|
|
||||||
'Bon marché',
|
|
||||||
'Modéré',
|
|
||||||
'Cher',
|
|
||||||
'Très cher',
|
|
||||||
];
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.7,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Niveau de prix',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: prices
|
|
||||||
.map(
|
|
||||||
(price) => ListTile(
|
|
||||||
title: Text(price),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedPrice = price;
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
trailing: _selectedPrice == price
|
|
||||||
? Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRatingFilterSheet() {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final ratings = [
|
|
||||||
'Note',
|
|
||||||
'4+ étoiles',
|
|
||||||
'3+ étoiles',
|
|
||||||
'2+ étoiles',
|
|
||||||
'1+ étoiles',
|
|
||||||
];
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.7,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Note minimale',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: ratings
|
|
||||||
.map(
|
|
||||||
(rating) => ListTile(
|
|
||||||
title: Text(rating),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedRating = rating;
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
_applyFilters();
|
|
||||||
},
|
|
||||||
trailing: _selectedRating == rating
|
|
||||||
? Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _applyFilters() {
|
|
||||||
String? category = _selectedCategory == 'Toutes les catégories'
|
|
||||||
? null
|
|
||||||
: _selectedCategory;
|
|
||||||
double? minRating = _getMinRatingFromString(_selectedRating);
|
|
||||||
|
|
||||||
context.read<ActivityBloc>().add(
|
|
||||||
FilterActivities(category: category, minRating: minRating),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
double? _getMinRatingFromString(String rating) {
|
|
||||||
switch (rating) {
|
|
||||||
case '4+ étoiles':
|
|
||||||
return 4.0;
|
|
||||||
case '3+ étoiles':
|
|
||||||
return 3.0;
|
|
||||||
case '2+ étoiles':
|
|
||||||
return 2.0;
|
|
||||||
case '1+ étoiles':
|
|
||||||
return 1.0;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showAddActivityBottomSheet() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
),
|
|
||||||
child: AddActivityBottomSheet(trip: widget.trip),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTripActivitiesTab() {
|
Widget _buildTripActivitiesTab() {
|
||||||
// Utiliser les données locales au lieu du BLoC
|
// Utiliser les données locales au lieu du BLoC
|
||||||
if (_isLoadingTripActivities) {
|
if (_isLoadingTripActivities) {
|
||||||
@@ -814,7 +560,9 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
Text(
|
Text(
|
||||||
'Recherche powered by Google Places',
|
'Recherche powered by Google Places',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha:0.6),
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1056,14 +804,18 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.location_on,
|
Icons.location_on,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
color: theme.colorScheme.onSurface.withValues(
|
||||||
|
alpha: 0.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
activity.address!,
|
activity.address!,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
color: theme.colorScheme.onSurface.withValues(
|
||||||
|
alpha: 0.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1248,8 +1000,38 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
|
|
||||||
void _voteForActivity(String activityId, int vote) {
|
void _voteForActivity(String activityId, int vote) {
|
||||||
// TODO: Récupérer l'ID utilisateur actuel
|
// TODO: Récupérer l'ID utilisateur actuel
|
||||||
// Pour l'instant, on utilise un ID temporaire
|
// Pour l'instant, on utilise l'ID du créateur du voyage pour que le vote compte
|
||||||
final userId = 'current_user_id';
|
final userId = widget.trip.createdBy;
|
||||||
|
|
||||||
|
// Vérifier si l'activité existe dans la liste locale pour vérifier le vote
|
||||||
|
// (car l'objet activity passé peut venir d'une liste filtrée ou autre)
|
||||||
|
final currentActivity = _tripActivities.firstWhere(
|
||||||
|
(a) => a.id == activityId,
|
||||||
|
orElse: () => _approvedActivities.firstWhere(
|
||||||
|
(a) => a.id == activityId,
|
||||||
|
orElse: () => Activity(
|
||||||
|
id: '',
|
||||||
|
tripId: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
category: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si l'activité a été trouvée et que l'utilisateur a déjà voté
|
||||||
|
if (currentActivity.id.isNotEmpty && currentActivity.hasUserVoted(userId)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Vous avez déjà voté pour cette activité'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.read<ActivityBloc>().add(
|
context.read<ActivityBloc>().add(
|
||||||
VoteForActivity(activityId: activityId, userId: userId, vote: vote),
|
VoteForActivity(activityId: activityId, userId: userId, vote: vote),
|
||||||
@@ -1285,12 +1067,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
loadingText: 'Ajout de ${activity.name}...',
|
loadingText: 'Ajout de ${activity.name}...',
|
||||||
onBackgroundTask: () async {
|
onBackgroundTask: () async {
|
||||||
// Ajouter l'activité au voyage
|
// Ajouter l'activité au voyage
|
||||||
context.read<ActivityBloc>().add(
|
context.read<ActivityBloc>().add(AddActivity(newActivity));
|
||||||
AddActivityAndRemoveFromSearch(
|
|
||||||
activity: newActivity,
|
|
||||||
googleActivityId: activity.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// Attendre que l'ajout soit complété
|
// Attendre que l'ajout soit complété
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
},
|
},
|
||||||
@@ -1514,4 +1291,19 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _performSearch(String query) {
|
||||||
|
// Basculer vers l'onglet suggestions
|
||||||
|
_tabController.animateTo(2);
|
||||||
|
|
||||||
|
// Déclencher la recherche textuelle
|
||||||
|
context.read<ActivityBloc>().add(
|
||||||
|
SearchActivitiesByText(
|
||||||
|
tripId: widget.trip.id!,
|
||||||
|
destination: widget.trip.location,
|
||||||
|
query: query,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_googleSearchPerformed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
275
lib/components/home/calendar/calendar_page.dart
Normal file
275
lib/components/home/calendar/calendar_page.dart
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import '../../../models/trip.dart';
|
||||||
|
import '../../../models/activity.dart';
|
||||||
|
import '../../../blocs/activity/activity_bloc.dart';
|
||||||
|
import '../../../blocs/activity/activity_state.dart';
|
||||||
|
import '../../../blocs/activity/activity_event.dart';
|
||||||
|
|
||||||
|
class CalendarPage extends StatefulWidget {
|
||||||
|
final Trip trip;
|
||||||
|
|
||||||
|
const CalendarPage({super.key, required this.trip});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CalendarPage> createState() => _CalendarPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalendarPageState extends State<CalendarPage> {
|
||||||
|
late DateTime _focusedDay;
|
||||||
|
DateTime? _selectedDay;
|
||||||
|
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusedDay = widget.trip.startDate;
|
||||||
|
_selectedDay = _focusedDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Activity> _getActivitiesForDay(DateTime day, List<Activity> activities) {
|
||||||
|
return activities.where((activity) {
|
||||||
|
if (activity.date == null) return false;
|
||||||
|
return isSameDay(activity.date, day);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Calendrier du voyage'),
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
foregroundColor: theme.colorScheme.onSurface,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: BlocBuilder<ActivityBloc, ActivityState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is ActivityLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Activity> allActivities = [];
|
||||||
|
if (state is ActivityLoaded) {
|
||||||
|
allActivities = state.activities;
|
||||||
|
} else if (state is ActivitySearchResults) {
|
||||||
|
// Fallback if we are in search state, though ideally we should be in loaded state
|
||||||
|
// This might happen if we navigate back and forth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter approved activities
|
||||||
|
final approvedActivities = allActivities.where((a) {
|
||||||
|
return a.isApprovedByAllParticipants([
|
||||||
|
...widget.trip.participants,
|
||||||
|
widget.trip.createdBy,
|
||||||
|
]);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final scheduledActivities = approvedActivities
|
||||||
|
.where((a) => a.date != null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final unscheduledActivities = approvedActivities
|
||||||
|
.where((a) => a.date == null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final selectedActivities = _getActivitiesForDay(
|
||||||
|
_selectedDay ?? _focusedDay,
|
||||||
|
scheduledActivities,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TableCalendar(
|
||||||
|
firstDay: DateTime.now().subtract(const Duration(days: 365)),
|
||||||
|
lastDay: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
focusedDay: _focusedDay,
|
||||||
|
calendarFormat: _calendarFormat,
|
||||||
|
selectedDayPredicate: (day) {
|
||||||
|
return isSameDay(_selectedDay, day);
|
||||||
|
},
|
||||||
|
onDaySelected: (selectedDay, focusedDay) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDay = selectedDay;
|
||||||
|
_focusedDay = focusedDay;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFormatChanged: (format) {
|
||||||
|
setState(() {
|
||||||
|
_calendarFormat = format;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPageChanged: (focusedDay) {
|
||||||
|
_focusedDay = focusedDay;
|
||||||
|
},
|
||||||
|
eventLoader: (day) {
|
||||||
|
return _getActivitiesForDay(day, scheduledActivities);
|
||||||
|
},
|
||||||
|
calendarBuilders: CalendarBuilders(
|
||||||
|
markerBuilder: (context, day, events) {
|
||||||
|
if (events.isEmpty) return null;
|
||||||
|
return Positioned(
|
||||||
|
bottom: 1,
|
||||||
|
child: Container(
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
calendarStyle: CalendarStyle(
|
||||||
|
todayDecoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 0.5),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
selectedDecoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Scheduled Activities for Selected Day
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'Activités du ${DateFormat('dd/MM/yyyy').format(_selectedDay!)}',
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: selectedActivities.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
'Aucune activité prévue',
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: selectedActivities.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final activity =
|
||||||
|
selectedActivities[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(activity.name),
|
||||||
|
subtitle: Text(activity.category),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ActivityBloc>().add(
|
||||||
|
UpdateActivityDate(
|
||||||
|
tripId: widget.trip.id!,
|
||||||
|
activityId: activity.id,
|
||||||
|
date: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
// Unscheduled Activities
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'À planifier',
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: unscheduledActivities.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
'Tout est planifié !',
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: unscheduledActivities.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final activity =
|
||||||
|
unscheduledActivities[index];
|
||||||
|
return Draggable<Activity>(
|
||||||
|
data: activity,
|
||||||
|
feedback: Material(
|
||||||
|
elevation: 4,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
color: theme.cardColor,
|
||||||
|
child: Text(activity.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
activity.name,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
if (_selectedDay != null) {
|
||||||
|
context
|
||||||
|
.read<ActivityBloc>()
|
||||||
|
.add(
|
||||||
|
UpdateActivityDate(
|
||||||
|
tripId: widget.trip.id!,
|
||||||
|
activityId: activity.id,
|
||||||
|
date: _selectedDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,17 +134,7 @@ class _HomeContentState extends State<HomeContent>
|
|||||||
: Colors.black,
|
: Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 16),
|
||||||
Text(
|
|
||||||
'Vos voyages',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white70
|
|
||||||
: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
if (tripState is TripLoading || tripState is TripCreated)
|
if (tripState is TripLoading || tripState is TripCreated)
|
||||||
_buildLoadingState()
|
_buildLoadingState()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:travel_mate/repositories/user_repository.dart';
|
|||||||
import 'package:travel_mate/repositories/account_repository.dart';
|
import 'package:travel_mate/repositories/account_repository.dart';
|
||||||
import 'package:travel_mate/models/group_member.dart';
|
import 'package:travel_mate/models/group_member.dart';
|
||||||
import 'package:travel_mate/components/activities/activities_page.dart';
|
import 'package:travel_mate/components/activities/activities_page.dart';
|
||||||
|
import 'package:travel_mate/components/home/calendar/calendar_page.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ShowTripDetailsContent extends StatefulWidget {
|
class ShowTripDetailsContent extends StatefulWidget {
|
||||||
@@ -204,7 +205,8 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback vers l'URL web
|
// Fallback vers l'URL web
|
||||||
final webUrl = 'https://www.google.com/maps/search/?api=1&query=$location';
|
final webUrl =
|
||||||
|
'https://www.google.com/maps/search/?api=1&query=$location';
|
||||||
final webUri = Uri.parse(webUrl);
|
final webUri = Uri.parse(webUrl);
|
||||||
if (await canLaunchUrl(webUri)) {
|
if (await canLaunchUrl(webUri)) {
|
||||||
await launchUrl(webUri, mode: LaunchMode.externalApplication);
|
await launchUrl(webUri, mode: LaunchMode.externalApplication);
|
||||||
@@ -212,7 +214,8 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_errorService.showError(
|
_errorService.showError(
|
||||||
message: 'Impossible d\'ouvrir Google Maps. Vérifiez que l\'application est installée.',
|
message:
|
||||||
|
'Impossible d\'ouvrir Google Maps. Vérifiez que l\'application est installée.',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.showError(
|
_errorService.showError(
|
||||||
@@ -243,12 +246,11 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_errorService.showError(
|
_errorService.showError(
|
||||||
message: 'Impossible d\'ouvrir Waze. Vérifiez que l\'application est installée.',
|
message:
|
||||||
|
'Impossible d\'ouvrir Waze. Vérifiez que l\'application est installée.',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.showError(
|
_errorService.showError(message: 'Erreur lors de l\'ouverture de Waze');
|
||||||
message: 'Erreur lors de l\'ouverture de Waze',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +260,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
final isDarkMode = theme.brightness == Brightness.dark;
|
final isDarkMode = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: isDarkMode ? theme.scaffoldBackgroundColor : Colors.grey[50],
|
backgroundColor: isDarkMode
|
||||||
|
? theme.scaffoldBackgroundColor
|
||||||
|
: Colors.grey[50],
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -300,11 +304,14 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: widget.trip.imageUrl != null && widget.trip.imageUrl!.isNotEmpty
|
child:
|
||||||
|
widget.trip.imageUrl != null &&
|
||||||
|
widget.trip.imageUrl!.isNotEmpty
|
||||||
? Image.network(
|
? Image.network(
|
||||||
widget.trip.imageUrl!,
|
widget.trip.imageUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) => _buildPlaceholderImage(),
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
_buildPlaceholderImage(),
|
||||||
)
|
)
|
||||||
: _buildPlaceholderImage(),
|
: _buildPlaceholderImage(),
|
||||||
),
|
),
|
||||||
@@ -318,7 +325,10 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
children: [
|
children: [
|
||||||
// Section "Départ dans X jours"
|
// Section "Départ dans X jours"
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.cardColor,
|
color: theme.cardColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -359,11 +369,15 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
Text(
|
Text(
|
||||||
'Départ dans',
|
'Départ dans',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
color: theme.colorScheme.onSurface.withValues(
|
||||||
|
alpha: 0.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
daysUntilTrip > 0 ? '$daysUntilTrip Jours' : 'Voyage terminé',
|
daysUntilTrip > 0
|
||||||
|
? '$daysUntilTrip Jours'
|
||||||
|
: 'Voyage terminé',
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
@@ -372,7 +386,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
Text(
|
Text(
|
||||||
widget.trip.formattedDates,
|
widget.trip.formattedDates,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
color: theme.colorScheme.onSurface.withValues(
|
||||||
|
alpha: 0.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -411,7 +427,13 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
icon: Icons.calendar_today,
|
icon: Icons.calendar_today,
|
||||||
title: 'Calendrier',
|
title: 'Calendrier',
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onTap: () => _showComingSoon('Calendrier'),
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
CalendarPage(trip: widget.trip),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
icon: Icons.local_activity,
|
icon: Icons.local_activity,
|
||||||
@@ -449,18 +471,11 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.location_city, size: 48, color: Colors.grey),
|
||||||
Icons.location_city,
|
|
||||||
size: 48,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Aucune image',
|
'Aucune image',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -510,11 +525,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
color: color.withValues(alpha: 0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(icon, color: color, size: 24),
|
||||||
icon,
|
|
||||||
color: color,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -627,10 +638,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||||
'Supprimer',
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -678,16 +686,13 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
...List.generate(
|
...List.generate(members.length, (index) {
|
||||||
members.length,
|
|
||||||
(index) {
|
|
||||||
final member = members[index];
|
final member = members[index];
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: _buildParticipantAvatar(member),
|
child: _buildParticipantAvatar(member),
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
// Bouton "+" pour ajouter un participant
|
// Bouton "+" pour ajouter un participant
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
@@ -705,7 +710,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final initials = member.pseudo.isNotEmpty
|
final initials = member.pseudo.isNotEmpty
|
||||||
? member.pseudo[0].toUpperCase()
|
? member.pseudo[0].toUpperCase()
|
||||||
: (member.firstName.isNotEmpty ? member.firstName[0].toUpperCase() : '?');
|
: (member.firstName.isNotEmpty
|
||||||
|
? member.firstName[0].toUpperCase()
|
||||||
|
: '?');
|
||||||
|
|
||||||
final name = member.pseudo.isNotEmpty ? member.pseudo : member.firstName;
|
final name = member.pseudo.isNotEmpty ? member.pseudo : member.firstName;
|
||||||
|
|
||||||
@@ -729,11 +736,14 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 28,
|
radius: 28,
|
||||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
|
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
|
||||||
backgroundImage: (member.profilePictureUrl != null &&
|
backgroundImage:
|
||||||
|
(member.profilePictureUrl != null &&
|
||||||
member.profilePictureUrl!.isNotEmpty)
|
member.profilePictureUrl!.isNotEmpty)
|
||||||
? NetworkImage(member.profilePictureUrl!)
|
? NetworkImage(member.profilePictureUrl!)
|
||||||
: null,
|
: null,
|
||||||
child: (member.profilePictureUrl == null || member.profilePictureUrl!.isEmpty)
|
child:
|
||||||
|
(member.profilePictureUrl == null ||
|
||||||
|
member.profilePictureUrl!.isEmpty)
|
||||||
? Text(
|
? Text(
|
||||||
initials,
|
initials,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -774,11 +784,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 28,
|
radius: 28,
|
||||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
child: Icon(
|
child: Icon(Icons.add, color: theme.colorScheme.primary, size: 28),
|
||||||
Icons.add,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -878,9 +884,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user.id == null) {
|
if (user.id == null) {
|
||||||
_errorService.showError(
|
_errorService.showError(message: 'ID utilisateur invalide');
|
||||||
message: 'ID utilisateur invalide',
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,16 +905,15 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
await _groupRepository.addMember(group.id, newMember);
|
await _groupRepository.addMember(group.id, newMember);
|
||||||
|
|
||||||
// Ajouter le membre au compte
|
// Ajouter le membre au compte
|
||||||
final account = await _accountRepository.getAccountByTripId(widget.trip.id!);
|
final account = await _accountRepository.getAccountByTripId(
|
||||||
|
widget.trip.id!,
|
||||||
|
);
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
await _accountRepository.addMemberToAccount(account.id, newMember);
|
await _accountRepository.addMemberToAccount(account.id, newMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettre à jour la liste des participants du voyage
|
// Mettre à jour la liste des participants du voyage
|
||||||
final newParticipants = [
|
final newParticipants = [...widget.trip.participants, user.id!];
|
||||||
...widget.trip.participants,
|
|
||||||
user.id!,
|
|
||||||
];
|
|
||||||
final updatedTrip = widget.trip.copyWith(
|
final updatedTrip = widget.trip.copyWith(
|
||||||
participants: newParticipants,
|
participants: newParticipants,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Activity {
|
|||||||
final Map<String, int> votes; // userId -> vote (1 pour pour, -1 pour contre)
|
final Map<String, int> votes; // userId -> vote (1 pour pour, -1 pour contre)
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
|
final DateTime? date; // Date prévue pour l'activité
|
||||||
|
|
||||||
Activity({
|
Activity({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -40,6 +41,7 @@ class Activity {
|
|||||||
this.votes = const {},
|
this.votes = const {},
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
this.date,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Calcule le score total des votes
|
/// Calcule le score total des votes
|
||||||
@@ -104,6 +106,8 @@ class Activity {
|
|||||||
Map<String, int>? votes,
|
Map<String, int>? votes,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
|
DateTime? date,
|
||||||
|
bool clearDate = false,
|
||||||
}) {
|
}) {
|
||||||
return Activity(
|
return Activity(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -124,6 +128,7 @@ class Activity {
|
|||||||
votes: votes ?? this.votes,
|
votes: votes ?? this.votes,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
date: clearDate ? null : (date ?? this.date),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +153,7 @@ class Activity {
|
|||||||
'votes': votes,
|
'votes': votes,
|
||||||
'createdAt': Timestamp.fromDate(createdAt),
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
'date': date != null ? Timestamp.fromDate(date!) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +178,7 @@ class Activity {
|
|||||||
votes: Map<String, int>.from(map['votes'] ?? {}),
|
votes: Map<String, int>.from(map['votes'] ?? {}),
|
||||||
createdAt: (map['createdAt'] as Timestamp).toDate(),
|
createdAt: (map['createdAt'] as Timestamp).toDate(),
|
||||||
updatedAt: (map['updatedAt'] as Timestamp).toDate(),
|
updatedAt: (map['updatedAt'] as Timestamp).toDate(),
|
||||||
|
date: map['date'] != null ? (map['date'] as Timestamp).toDate() : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ class ActivityRepository {
|
|||||||
try {
|
try {
|
||||||
print('ActivityRepository: Ajout d\'une activité: ${activity.name}');
|
print('ActivityRepository: Ajout d\'une activité: ${activity.name}');
|
||||||
|
|
||||||
final docRef = await _firestore.collection(_collection).add(activity.toMap());
|
final docRef = await _firestore
|
||||||
|
.collection(_collection)
|
||||||
|
.add(activity.toMap());
|
||||||
|
|
||||||
// Mettre à jour l'activité avec l'ID généré
|
// Mettre à jour l'activité avec l'ID généré
|
||||||
await docRef.update({'id': docRef.id});
|
await docRef.update({'id': docRef.id});
|
||||||
@@ -27,7 +29,10 @@ class ActivityRepository {
|
|||||||
return docRef.id;
|
return docRef.id;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur lors de l\'ajout: $e');
|
print('ActivityRepository: Erreur lors de l\'ajout: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur ajout activité: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur ajout activité: $e',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +40,9 @@ class ActivityRepository {
|
|||||||
/// Récupère toutes les activités d'un voyage
|
/// Récupère toutes les activités d'un voyage
|
||||||
Future<List<Activity>> getActivitiesByTrip(String tripId) async {
|
Future<List<Activity>> getActivitiesByTrip(String tripId) async {
|
||||||
try {
|
try {
|
||||||
print('ActivityRepository: Récupération des activités pour le voyage: $tripId');
|
print(
|
||||||
|
'ActivityRepository: Récupération des activités pour le voyage: $tripId',
|
||||||
|
);
|
||||||
|
|
||||||
// Modifié pour éviter l'erreur d'index composite
|
// Modifié pour éviter l'erreur d'index composite
|
||||||
// On récupère d'abord par tripId, puis on trie en mémoire
|
// On récupère d'abord par tripId, puis on trie en mémoire
|
||||||
@@ -55,15 +62,26 @@ class ActivityRepository {
|
|||||||
return activities;
|
return activities;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur lors de la récupération: $e');
|
print('ActivityRepository: Erreur lors de la récupération: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur récupération activités: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur récupération activités: $e',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Récupère une activité par son ID (alias pour getActivityById pour compatibilité)
|
||||||
|
Future<Activity?> getActivity(String tripId, String activityId) async {
|
||||||
|
return getActivityById(activityId);
|
||||||
|
}
|
||||||
|
|
||||||
/// Récupère une activité par son ID
|
/// Récupère une activité par son ID
|
||||||
Future<Activity?> getActivityById(String activityId) async {
|
Future<Activity?> getActivityById(String activityId) async {
|
||||||
try {
|
try {
|
||||||
final doc = await _firestore.collection(_collection).doc(activityId).get();
|
final doc = await _firestore
|
||||||
|
.collection(_collection)
|
||||||
|
.doc(activityId)
|
||||||
|
.get();
|
||||||
|
|
||||||
if (doc.exists) {
|
if (doc.exists) {
|
||||||
return Activity.fromSnapshot(doc);
|
return Activity.fromSnapshot(doc);
|
||||||
@@ -72,7 +90,10 @@ class ActivityRepository {
|
|||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur récupération activité: $e');
|
print('ActivityRepository: Erreur récupération activité: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur récupération activité: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur récupération activité: $e',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +112,10 @@ class ActivityRepository {
|
|||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur lors de la mise à jour: $e');
|
print('ActivityRepository: Erreur lors de la mise à jour: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur mise à jour activité: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur mise à jour activité: $e',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,24 +131,37 @@ class ActivityRepository {
|
|||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur lors de la suppression: $e');
|
print('ActivityRepository: Erreur lors de la suppression: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur suppression activité: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur suppression activité: $e',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vote pour une activité
|
/// Vote pour une activité
|
||||||
Future<bool> voteForActivity(String activityId, String userId, int vote) async {
|
Future<bool> voteForActivity(
|
||||||
|
String activityId,
|
||||||
|
String userId,
|
||||||
|
int vote,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
// Validation des paramètres
|
// Validation des paramètres
|
||||||
if (activityId.isEmpty) {
|
if (activityId.isEmpty) {
|
||||||
print('ActivityRepository: ID d\'activité vide');
|
print('ActivityRepository: ID d\'activité vide');
|
||||||
_errorService.logError('activity_repository', 'ID d\'activité vide pour le vote');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'ID d\'activité vide pour le vote',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId.isEmpty) {
|
if (userId.isEmpty) {
|
||||||
print('ActivityRepository: ID d\'utilisateur vide');
|
print('ActivityRepository: ID d\'utilisateur vide');
|
||||||
_errorService.logError('activity_repository', 'ID d\'utilisateur vide pour le vote');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'ID d\'utilisateur vide pour le vote',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +222,10 @@ class ActivityRepository {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur stream activités: $e');
|
print('ActivityRepository: Erreur stream activités: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur stream activités: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur stream activités: $e',
|
||||||
|
);
|
||||||
return Stream.value([]);
|
return Stream.value([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +233,9 @@ class ActivityRepository {
|
|||||||
/// Ajoute plusieurs activités en lot
|
/// Ajoute plusieurs activités en lot
|
||||||
Future<List<String>> addActivitiesBatch(List<Activity> activities) async {
|
Future<List<String>> addActivitiesBatch(List<Activity> activities) async {
|
||||||
try {
|
try {
|
||||||
print('ActivityRepository: Ajout en lot de ${activities.length} activités');
|
print(
|
||||||
|
'ActivityRepository: Ajout en lot de ${activities.length} activités',
|
||||||
|
);
|
||||||
|
|
||||||
final batch = _firestore.batch();
|
final batch = _firestore.batch();
|
||||||
final addedIds = <String>[];
|
final addedIds = <String>[];
|
||||||
@@ -217,9 +259,14 @@ class ActivityRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Recherche des activités par catégorie
|
/// Recherche des activités par catégorie
|
||||||
Future<List<Activity>> getActivitiesByCategory(String tripId, String category) async {
|
Future<List<Activity>> getActivitiesByCategory(
|
||||||
|
String tripId,
|
||||||
|
String category,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
print('ActivityRepository: Recherche par catégorie: $category pour le voyage: $tripId');
|
print(
|
||||||
|
'ActivityRepository: Recherche par catégorie: $category pour le voyage: $tripId',
|
||||||
|
);
|
||||||
|
|
||||||
// Récupérer toutes les activités du voyage puis filtrer en mémoire
|
// Récupérer toutes les activités du voyage puis filtrer en mémoire
|
||||||
final querySnapshot = await _firestore
|
final querySnapshot = await _firestore
|
||||||
@@ -238,13 +285,19 @@ class ActivityRepository {
|
|||||||
return activities;
|
return activities;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('ActivityRepository: Erreur recherche par catégorie: $e');
|
print('ActivityRepository: Erreur recherche par catégorie: $e');
|
||||||
_errorService.logError('activity_repository', 'Erreur recherche par catégorie: $e');
|
_errorService.logError(
|
||||||
|
'activity_repository',
|
||||||
|
'Erreur recherche par catégorie: $e',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère les activités les mieux notées d'un voyage
|
/// Récupère les activités les mieux notées d'un voyage
|
||||||
Future<List<Activity>> getTopRatedActivities(String tripId, {int limit = 10}) async {
|
Future<List<Activity>> getTopRatedActivities(
|
||||||
|
String tripId, {
|
||||||
|
int limit = 10,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final activities = await getActivitiesByTrip(tripId);
|
final activities = await getActivitiesByTrip(tripId);
|
||||||
|
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -1024,6 +1024,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
simple_gesture_detector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: simple_gesture_detector
|
||||||
|
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1125,6 +1133,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.0"
|
||||||
|
table_calendar:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: table_calendar
|
||||||
|
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies:
|
|||||||
location: ^8.0.1
|
location: ^8.0.1
|
||||||
flutter_bloc : ^9.1.1
|
flutter_bloc : ^9.1.1
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
|
table_calendar: ^3.1.0
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Reference in New Issue
Block a user