feat: Add calendar page, enhance activity search and approval logic, and refactor activity filtering UI.
This commit is contained in:
@@ -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