feat: Add calendar page, enhance activity search and approval logic, and refactor activity filtering UI.
This commit is contained in:
@@ -6,7 +6,7 @@ import '../../blocs/activity/activity_state.dart';
|
||||
import '../../models/trip.dart';
|
||||
import '../../models/activity.dart';
|
||||
import '../../services/activity_cache_service.dart';
|
||||
import '../activities/add_activity_bottom_sheet.dart';
|
||||
|
||||
import '../loading/laoding_content.dart';
|
||||
|
||||
class ActivitiesPage extends StatefulWidget {
|
||||
@@ -23,9 +23,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
late TabController _tabController;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ActivityCacheService _cacheService = ActivityCacheService();
|
||||
String _selectedCategory = 'Toutes les catégories';
|
||||
String _selectedPrice = 'Prix';
|
||||
String _selectedRating = 'Note';
|
||||
|
||||
// Cache pour éviter de recharger les données
|
||||
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
|
||||
if (_tabController.index == 2 && !_googleSearchPerformed) {
|
||||
// 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) {
|
||||
// Restaurer les activités en cache dans le BLoC
|
||||
context.read<ActivityBloc>().add(
|
||||
@@ -144,13 +143,13 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
|
||||
// Ajouter l'activité à la liste locale des activités du voyage
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_tripActivities.add(state.activity);
|
||||
});
|
||||
|
||||
|
||||
// Afficher un feedback de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -174,10 +173,26 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
// Stocker les activités localement
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_tripActivities = state.activities;
|
||||
_approvedActivities = state.activities
|
||||
.where((a) => a.totalVotes > 0)
|
||||
final allActivities = state.filteredActivities;
|
||||
|
||||
_approvedActivities = allActivities
|
||||
.where(
|
||||
(a) => a.isApprovedByAllParticipants([
|
||||
...widget.trip.participants,
|
||||
widget.trip.createdBy,
|
||||
]),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_tripActivities = allActivities
|
||||
.where(
|
||||
(a) => !a.isApprovedByAllParticipants([
|
||||
...widget.trip.participants,
|
||||
widget.trip.createdBy,
|
||||
]),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_isLoadingTripActivities = false;
|
||||
});
|
||||
|
||||
@@ -189,6 +204,37 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
}
|
||||
|
||||
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)
|
||||
// et pas pour les rechargements automatiques
|
||||
if (state.searchResults.length <= 6 && !_autoReloadInProgress) {
|
||||
@@ -220,21 +266,13 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
elevation: 0,
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: _showAddActivityBottomSheet,
|
||||
),
|
||||
],
|
||||
actions: [],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
_buildSearchBar(theme),
|
||||
|
||||
// Filtres
|
||||
_buildFilters(theme),
|
||||
|
||||
// Onglets de catégories
|
||||
_buildCategoryTabs(theme),
|
||||
|
||||
@@ -260,7 +298,9 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TextField(
|
||||
@@ -268,11 +308,11 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher restaurants, musées...',
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@@ -280,95 +320,32 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// TODO: Implémenter la recherche
|
||||
onSubmitted: (value) {
|
||||
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) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha:0.3),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: theme.colorScheme.onSurface.withValues(alpha:0.7),
|
||||
unselectedLabelColor: theme.colorScheme.onSurface.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
indicator: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
@@ -376,246 +353,15 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: const [
|
||||
Tab(text: 'Activités du voyage'),
|
||||
Tab(text: 'Activités approuvées'),
|
||||
Tab(text: 'Suggestions Google'),
|
||||
Tab(text: 'Voyage'),
|
||||
Tab(text: 'Approuvées'),
|
||||
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() {
|
||||
// Utiliser les données locales au lieu du BLoC
|
||||
if (_isLoadingTripActivities) {
|
||||
@@ -814,7 +560,9 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
Text(
|
||||
'Recherche powered by Google Places',
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -845,7 +593,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.7),
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -977,7 +725,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(alpha:0.1),
|
||||
color: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
@@ -1015,7 +763,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha:0.1),
|
||||
color: Colors.amber.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
@@ -1043,7 +791,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
Text(
|
||||
activity.description,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.8),
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -1056,14 +804,18 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
||||
color: theme.colorScheme.onSurface.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
activity.address!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
||||
color: theme.colorScheme.onSurface.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1084,10 +836,10 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha:0.1),
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withValues(alpha:0.3),
|
||||
color: Colors.orange.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -1140,7 +892,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha:0.1),
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
@@ -1170,7 +922,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha:0.1),
|
||||
color: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
@@ -1248,8 +1000,38 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
|
||||
void _voteForActivity(String activityId, int vote) {
|
||||
// TODO: Récupérer l'ID utilisateur actuel
|
||||
// Pour l'instant, on utilise un ID temporaire
|
||||
final userId = 'current_user_id';
|
||||
// Pour l'instant, on utilise l'ID du créateur du voyage pour que le vote compte
|
||||
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(
|
||||
VoteForActivity(activityId: activityId, userId: userId, vote: vote),
|
||||
@@ -1285,12 +1067,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
||||
loadingText: 'Ajout de ${activity.name}...',
|
||||
onBackgroundTask: () async {
|
||||
// Ajouter l'activité au voyage
|
||||
context.read<ActivityBloc>().add(
|
||||
AddActivityAndRemoveFromSearch(
|
||||
activity: newActivity,
|
||||
googleActivityId: activity.id,
|
||||
),
|
||||
);
|
||||
context.read<ActivityBloc>().add(AddActivity(newActivity));
|
||||
// Attendre que l'ajout soit complété
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Vos voyages',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white70
|
||||
: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (tripState is TripLoading || tripState is TripCreated)
|
||||
_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/models/group_member.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';
|
||||
|
||||
class ShowTripDetailsContent extends StatefulWidget {
|
||||
@@ -94,7 +95,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
// Méthode pour afficher le dialogue de sélection de carte
|
||||
void _showMapOptions() {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
@@ -193,7 +194,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
// Méthode pour ouvrir Google Maps
|
||||
Future<void> _openGoogleMaps() async {
|
||||
final location = Uri.encodeComponent(widget.trip.location);
|
||||
|
||||
|
||||
try {
|
||||
// Essayer d'abord l'URL scheme pour l'app mobile
|
||||
final appUrl = 'comgooglemaps://?q=$location';
|
||||
@@ -202,17 +203,19 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
await launchUrl(appUri);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
if (await canLaunchUrl(webUri)) {
|
||||
await launchUrl(webUri, mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_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) {
|
||||
_errorService.showError(
|
||||
@@ -224,7 +227,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
// Méthode pour ouvrir Waze
|
||||
Future<void> _openWaze() async {
|
||||
final location = Uri.encodeComponent(widget.trip.location);
|
||||
|
||||
|
||||
try {
|
||||
// Essayer d'abord l'URL scheme pour l'app mobile
|
||||
final appUrl = 'waze://?q=$location';
|
||||
@@ -233,7 +236,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
await launchUrl(appUri);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Fallback vers l'URL web
|
||||
final webUrl = 'https://waze.com/ul?q=$location';
|
||||
final webUri = Uri.parse(webUrl);
|
||||
@@ -241,14 +244,13 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
await launchUrl(webUri, mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_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) {
|
||||
_errorService.showError(
|
||||
message: 'Erreur lors de l\'ouverture de Waze',
|
||||
);
|
||||
_errorService.showError(message: 'Erreur lors de l\'ouverture de Waze');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,9 +258,11 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDarkMode = theme.brightness == Brightness.dark;
|
||||
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: isDarkMode ? theme.scaffoldBackgroundColor : Colors.grey[50],
|
||||
backgroundColor: isDarkMode
|
||||
? theme.scaffoldBackgroundColor
|
||||
: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
@@ -292,7 +296,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha:0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@@ -300,16 +304,19 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: widget.trip.imageUrl != null && widget.trip.imageUrl!.isNotEmpty
|
||||
child:
|
||||
widget.trip.imageUrl != null &&
|
||||
widget.trip.imageUrl!.isNotEmpty
|
||||
? Image.network(
|
||||
widget.trip.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => _buildPlaceholderImage(),
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
_buildPlaceholderImage(),
|
||||
)
|
||||
: _buildPlaceholderImage(),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Contenu principal
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -318,21 +325,24 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
children: [
|
||||
// Section "Départ dans X jours"
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isDarkMode
|
||||
? Colors.white.withValues(alpha:0.1)
|
||||
: Colors.black.withValues(alpha:0.1),
|
||||
color: isDarkMode
|
||||
? Colors.white.withValues(alpha: 0.1)
|
||||
: Colors.black.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDarkMode
|
||||
? Colors.black.withValues(alpha:0.3)
|
||||
: Colors.black.withValues(alpha:0.1),
|
||||
color: isDarkMode
|
||||
? Colors.black.withValues(alpha: 0.3)
|
||||
: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: isDarkMode ? 8 : 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -343,7 +353,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal.withValues(alpha:0.1),
|
||||
color: Colors.teal.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
@@ -359,11 +369,15 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
Text(
|
||||
'Départ dans',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
||||
color: theme.colorScheme.onSurface.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
daysUntilTrip > 0 ? '$daysUntilTrip Jours' : 'Voyage terminé',
|
||||
daysUntilTrip > 0
|
||||
? '$daysUntilTrip Jours'
|
||||
: 'Voyage terminé',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
@@ -372,7 +386,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
Text(
|
||||
widget.trip.formattedDates,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha:0.6),
|
||||
color: theme.colorScheme.onSurface.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -380,9 +396,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
// Section Participants
|
||||
Text(
|
||||
'Participants',
|
||||
@@ -392,12 +408,12 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
|
||||
// Afficher les participants avec leurs images
|
||||
_buildParticipantsSection(),
|
||||
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
|
||||
// Grille d'actions
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
@@ -411,7 +427,13 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
icon: Icons.calendar_today,
|
||||
title: 'Calendrier',
|
||||
color: Colors.blue,
|
||||
onTap: () => _showComingSoon('Calendrier'),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
CalendarPage(trip: widget.trip),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionButton(
|
||||
icon: Icons.local_activity,
|
||||
@@ -449,18 +471,11 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_city,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Icon(Icons.location_city, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucune image',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -476,7 +491,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final isDarkMode = theme.brightness == Brightness.dark;
|
||||
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -486,16 +501,16 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
color: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isDarkMode
|
||||
? Colors.white.withValues(alpha:0.1)
|
||||
: Colors.black.withValues(alpha:0.1),
|
||||
color: isDarkMode
|
||||
? Colors.white.withValues(alpha: 0.1)
|
||||
: Colors.black.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDarkMode
|
||||
? Colors.black.withValues(alpha:0.3)
|
||||
: Colors.black.withValues(alpha:0.1),
|
||||
color: isDarkMode
|
||||
? Colors.black.withValues(alpha: 0.3)
|
||||
: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: isDarkMode ? 8 : 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -510,11 +525,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
@@ -542,7 +553,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
|
||||
void _showOptionsMenu() {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: theme.bottomSheetTheme.backgroundColor,
|
||||
@@ -594,7 +605,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
|
||||
void _showDeleteConfirmation() {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -627,10 +638,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: const Text(
|
||||
'Supprimer',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -678,16 +686,13 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
members.length,
|
||||
(index) {
|
||||
final member = members[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: _buildParticipantAvatar(member),
|
||||
);
|
||||
},
|
||||
),
|
||||
...List.generate(members.length, (index) {
|
||||
final member = members[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: _buildParticipantAvatar(member),
|
||||
);
|
||||
}),
|
||||
// Bouton "+" pour ajouter un participant
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
@@ -705,7 +710,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
final theme = Theme.of(context);
|
||||
final initials = member.pseudo.isNotEmpty
|
||||
? 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;
|
||||
|
||||
@@ -729,11 +736,14 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
child: CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
|
||||
backgroundImage: (member.profilePictureUrl != null &&
|
||||
member.profilePictureUrl!.isNotEmpty)
|
||||
backgroundImage:
|
||||
(member.profilePictureUrl != null &&
|
||||
member.profilePictureUrl!.isNotEmpty)
|
||||
? NetworkImage(member.profilePictureUrl!)
|
||||
: null,
|
||||
child: (member.profilePictureUrl == null || member.profilePictureUrl!.isEmpty)
|
||||
child:
|
||||
(member.profilePictureUrl == null ||
|
||||
member.profilePictureUrl!.isEmpty)
|
||||
? Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
@@ -774,11 +784,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
child: CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 28,
|
||||
),
|
||||
child: Icon(Icons.add, color: theme.colorScheme.primary, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -869,7 +875,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
try {
|
||||
// Chercher l'utilisateur par email
|
||||
final user = await _userRepository.getUserByEmail(email);
|
||||
|
||||
|
||||
if (user == null) {
|
||||
_errorService.showError(
|
||||
message: 'Utilisateur non trouvé avec cet email',
|
||||
@@ -878,9 +884,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
}
|
||||
|
||||
if (user.id == null) {
|
||||
_errorService.showError(
|
||||
message: 'ID utilisateur invalide',
|
||||
);
|
||||
_errorService.showError(message: 'ID utilisateur invalide');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -901,20 +905,19 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
||||
await _groupRepository.addMember(group.id, newMember);
|
||||
|
||||
// Ajouter le membre au compte
|
||||
final account = await _accountRepository.getAccountByTripId(widget.trip.id!);
|
||||
final account = await _accountRepository.getAccountByTripId(
|
||||
widget.trip.id!,
|
||||
);
|
||||
if (account != null) {
|
||||
await _accountRepository.addMemberToAccount(account.id, newMember);
|
||||
}
|
||||
|
||||
// Mettre à jour la liste des participants du voyage
|
||||
final newParticipants = [
|
||||
...widget.trip.participants,
|
||||
user.id!,
|
||||
];
|
||||
final newParticipants = [...widget.trip.participants, user.id!];
|
||||
final updatedTrip = widget.trip.copyWith(
|
||||
participants: newParticipants,
|
||||
);
|
||||
|
||||
|
||||
if (mounted) {
|
||||
context.read<TripBloc>().add(
|
||||
TripUpdateRequested(trip: updatedTrip),
|
||||
|
||||
Reference in New Issue
Block a user