Files
TravelMate/lib/components/home/show_trip_details_content.dart

1142 lines
37 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/blocs/trip/trip_bloc.dart';
import 'package:travel_mate/blocs/trip/trip_event.dart';
import 'package:travel_mate/blocs/activity/activity_bloc.dart';
import 'package:travel_mate/blocs/activity/activity_event.dart';
import 'package:travel_mate/components/home/create_trip_content.dart';
import 'package:travel_mate/models/trip.dart';
import 'package:travel_mate/components/map/map_content.dart';
import 'package:travel_mate/services/error_service.dart';
import 'package:travel_mate/services/activity_cache_service.dart';
import 'package:travel_mate/repositories/group_repository.dart';
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 {
final Trip trip;
const ShowTripDetailsContent({super.key, required this.trip});
@override
State<ShowTripDetailsContent> createState() => _ShowTripDetailsContentState();
}
class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
final ErrorService _errorService = ErrorService();
final ActivityCacheService _cacheService = ActivityCacheService();
final GroupRepository _groupRepository = GroupRepository();
final UserRepository _userRepository = UserRepository();
final AccountRepository _accountRepository = AccountRepository();
@override
void initState() {
super.initState();
// Lancer la recherche d'activités Google en arrière-plan
_preloadGoogleActivities();
}
/// Précharger les activités Google en arrière-plan
void _preloadGoogleActivities() {
// Attendre un moment avant de lancer la recherche pour ne pas bloquer l'UI
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted && widget.trip.id != null) {
// Vérifier si on a déjà des activités en cache
if (_cacheService.hasCachedActivities(widget.trip.id!)) {
return; // Utiliser le cache
}
// Sinon, lancer la recherche avec le maximum d'activités
context.read<ActivityBloc>().add(
widget.trip.hasCoordinates
? SearchActivitiesWithCoordinates(
tripId: widget.trip.id!,
latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!,
category: null,
maxResults: 100, // Charger le maximum d'activités possible
reset: true,
)
: SearchActivities(
tripId: widget.trip.id!,
destination: widget.trip.location,
category: null,
maxResults: 100, // Charger le maximum d'activités possible
reset: true,
),
);
}
});
}
// Calculer les jours restants avant le voyage
int get daysUntilTrip {
final now = DateTime.now();
final tripStart = widget.trip.startDate;
final difference = tripStart.difference(now).inDays;
return difference > 0 ? difference : 0;
}
// Méthode pour ouvrir la carte interne
void _openInternalMap() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
MapContent(initialSearchQuery: widget.trip.location),
),
);
}
// Méthode pour afficher le dialogue de sélection de carte
void _showMapOptions() {
final theme = Theme.of(context);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface,
title: Text(
'Ouvrir la carte',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Choisissez comment vous souhaitez ouvrir la carte :',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 20),
// Options centrées verticalement
Column(
children: [
// Carte de l'application
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_openInternalMap();
},
icon: const Icon(Icons.map),
label: const Text('Carte de l\'app'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(height: 12),
// Google Maps
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_openGoogleMaps();
},
icon: const Icon(Icons.directions),
label: const Text('Google Maps'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(height: 12),
// Waze
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_openWaze();
},
icon: const Icon(Icons.navigation),
label: const Text('Waze'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: TextStyle(color: theme.colorScheme.primary),
),
),
],
);
},
);
}
// 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';
final appUri = Uri.parse(appUrl);
if (await canLaunchUrl(appUri)) {
await launchUrl(appUri);
return;
}
// Fallback vers l'URL web
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.',
);
} catch (e) {
_errorService.showError(
message: 'Erreur lors de l\'ouverture de Google Maps',
);
}
}
// 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';
final appUri = Uri.parse(appUrl);
if (await canLaunchUrl(appUri)) {
await launchUrl(appUri);
return;
}
// Fallback vers l'URL web
final webUrl = 'https://waze.com/ul?q=$location';
final webUri = Uri.parse(webUrl);
if (await canLaunchUrl(webUri)) {
await launchUrl(webUri, mode: LaunchMode.externalApplication);
return;
}
_errorService.showError(
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');
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode
? theme.scaffoldBackgroundColor
: Colors.grey[50],
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
widget.trip.title,
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
actions: [
IconButton(
icon: Icon(Icons.more_vert, color: theme.colorScheme.onSurface),
onPressed: () => _showOptionsMenu(),
),
],
),
body: SingleChildScrollView(
child: Column(
children: [
// Image du voyage
Container(
height: 250,
width: double.infinity,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child:
widget.trip.imageUrl != null &&
widget.trip.imageUrl!.isNotEmpty
? Image.network(
widget.trip.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_buildPlaceholderImage(),
)
: _buildPlaceholderImage(),
),
),
// Contenu principal
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section "Départ dans X jours"
Container(
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),
width: 1,
),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.1),
blurRadius: isDarkMode ? 8 : 5,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.teal.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.flight_takeoff,
color: Colors.teal,
size: 20,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Départ dans',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.6,
),
),
),
Text(
daysUntilTrip > 0
? '$daysUntilTrip Jours'
: 'Voyage terminé',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
Text(
widget.trip.formattedDates,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.6,
),
),
),
],
),
],
),
),
const SizedBox(height: 24),
// Section Participants
Text(
'Participants',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 12),
// Afficher les participants avec leurs images
_buildParticipantsSection(),
const SizedBox(height: 32),
// Grille d'actions
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildActionButton(
icon: Icons.calendar_today,
title: 'Calendrier',
color: Colors.blue,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CalendarPage(trip: widget.trip),
),
),
),
_buildActionButton(
icon: Icons.local_activity,
title: 'Activités',
color: Colors.green,
onTap: () => _navigateToActivities(),
),
_buildActionButton(
icon: Icons.account_balance_wallet,
title: 'Dépenses',
color: Colors.orange,
onTap: () => _showComingSoon('Dépenses'),
),
_buildActionButton(
icon: Icons.map,
title: 'Ouvrir la carte',
color: Colors.purple,
onTap: _showMapOptions,
),
],
),
const SizedBox(height: 32),
_buildNextActivitiesSection(),
_buildExpensesCard(),
],
),
),
],
),
),
);
}
Widget _buildPlaceholderImage() {
return Container(
color: Colors.grey[200],
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_city, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text(
'Aucune image',
style: TextStyle(color: Colors.grey, fontSize: 14),
),
],
),
),
);
}
Widget _buildActionButton({
required IconData icon,
required String title,
required Color color,
required VoidCallback onTap,
}) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
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),
width: 1,
),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.1),
blurRadius: isDarkMode ? 8 : 5,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
title,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
void _showComingSoon(String feature) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$feature - Fonctionnalité à venir'),
backgroundColor: Colors.blue,
),
);
}
void _showOptionsMenu() {
final theme = Theme.of(context);
showModalBottomSheet(
context: context,
backgroundColor: theme.bottomSheetTheme.backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.edit, color: theme.colorScheme.primary),
title: Text(
'Modifier le voyage',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CreateTripContent(tripToEdit: widget.trip),
),
);
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(
'Supprimer le voyage',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
onTap: () {
Navigator.pop(context);
_showDeleteConfirmation();
},
),
],
),
),
);
}
void _showDeleteConfirmation() {
final theme = Theme.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor:
theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface,
title: Text(
'Confirmer la suppression',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
content: Text(
'Êtes-vous sûr de vouloir supprimer ce voyage ? Cette action est irréversible.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: TextStyle(color: theme.colorScheme.primary),
),
),
TextButton(
onPressed: () {
context.read<TripBloc>().add(
TripDeleteRequested(tripId: widget.trip.id!),
);
Navigator.pop(context);
Navigator.pop(context, true);
},
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
),
],
),
);
}
/// Construire la section des participants avec leurs images de profil
Widget _buildParticipantsSection() {
// Vérifier que le trip a un ID
if (widget.trip.id == null || widget.trip.id!.isEmpty) {
return const Center(child: Text('Aucun participant'));
}
return FutureBuilder(
future: _groupRepository.getGroupByTripId(widget.trip.id!),
builder: (context, groupSnapshot) {
if (groupSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (groupSnapshot.hasError ||
!groupSnapshot.hasData ||
groupSnapshot.data == null) {
return const Center(child: Text('Aucun participant'));
}
final groupId = groupSnapshot.data!.id;
return StreamBuilder<List<GroupMember>>(
stream: _groupRepository.watchGroupMembers(groupId),
builder: (context, snapshot) {
// En attente
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// Erreur
if (snapshot.hasError) {
return Center(
child: Text(
'Erreur: ${snapshot.error}',
style: TextStyle(color: Colors.red),
),
);
}
final members = snapshot.data ?? [];
if (members.isEmpty) {
return const Center(child: Text('Aucun participant'));
}
return SingleChildScrollView(
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),
);
}),
// Bouton "+" pour ajouter un participant
Padding(
padding: const EdgeInsets.only(right: 12),
child: _buildAddParticipantButton(),
),
],
),
);
},
);
},
);
}
/// Construire un avatar pour un participant
Widget _buildParticipantAvatar(dynamic member) {
final theme = Theme.of(context);
final initials = member.pseudo.isNotEmpty
? member.pseudo[0].toUpperCase()
: (member.firstName.isNotEmpty
? member.firstName[0].toUpperCase()
: '?');
final name = member.pseudo.isNotEmpty ? member.pseudo : member.firstName;
return Tooltip(
message: name,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: CircleAvatar(
radius: 28,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
backgroundImage:
(member.profilePictureUrl != null &&
member.profilePictureUrl!.isNotEmpty)
? NetworkImage(member.profilePictureUrl!)
: null,
child:
(member.profilePictureUrl == null ||
member.profilePictureUrl!.isEmpty)
? Text(
initials,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
)
: null,
),
),
);
}
/// Construire le bouton pour ajouter un participant
Widget _buildAddParticipantButton() {
final theme = Theme.of(context);
return Tooltip(
message: 'Ajouter un participant',
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: GestureDetector(
onTap: _showAddParticipantDialog,
child: CircleAvatar(
radius: 28,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Icon(Icons.add, color: theme.colorScheme.primary, size: 28),
),
),
),
);
}
/// Afficher le dialogue pour ajouter un participant
void _showAddParticipantDialog() {
final theme = Theme.of(context);
final TextEditingController emailController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface,
title: Text(
'Ajouter un participant',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Entrez l\'email du participant à ajouter :',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 16),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'participant@example.com',
hintStyle: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
style: TextStyle(color: theme.colorScheme.onSurface),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: TextStyle(color: theme.colorScheme.primary),
),
),
TextButton(
onPressed: () {
if (emailController.text.isNotEmpty) {
_addParticipantByEmail(emailController.text);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez entrer un email valide'),
backgroundColor: Colors.red,
),
);
}
},
child: Text(
'Ajouter',
style: TextStyle(color: theme.colorScheme.primary),
),
),
],
);
},
);
}
/// Ajouter un participant par email
Future<void> _addParticipantByEmail(String email) async {
try {
// Chercher l'utilisateur par email
final user = await _userRepository.getUserByEmail(email);
if (user == null) {
_errorService.showError(
message: 'Utilisateur non trouvé avec cet email',
);
return;
}
if (user.id == null) {
_errorService.showError(message: 'ID utilisateur invalide');
return;
}
// Ajouter l'utilisateur au groupe
if (widget.trip.id != null) {
final group = await _groupRepository.getGroupByTripId(widget.trip.id!);
if (group != null) {
// Créer un GroupMember à partir du User
final newMember = GroupMember(
userId: user.id!,
firstName: user.prenom,
lastName: user.nom,
pseudo: user.prenom,
profilePictureUrl: user.profilePictureUrl,
);
// Ajouter le membre au groupe
await _groupRepository.addMember(group.id, newMember);
// Ajouter le membre au compte
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 updatedTrip = widget.trip.copyWith(
participants: newParticipants,
);
if (mounted) {
context.read<TripBloc>().add(
TripUpdateRequested(trip: updatedTrip),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${user.prenom} a été ajouté au voyage'),
backgroundColor: Colors.green,
),
);
// Rafraîchir la page
setState(() {});
}
}
}
} catch (e) {
_errorService.showError(
message: 'Erreur lors de l\'ajout du participant: $e',
);
}
}
void _navigateToActivities() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ActivitiesPage(trip: widget.trip),
),
);
}
Widget _buildNextActivitiesSection() {
final theme = Theme.of(context);
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Prochaines activités',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
TextButton(
onPressed: () => _navigateToActivities(),
child: Text(
'Tout voir',
style: TextStyle(
color: Colors.teal,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 8),
_buildActivityCard(
title: 'Visite du Colisée',
date: '11 août, 10:00',
icon: Icons.museum,
),
const SizedBox(height: 12),
_buildActivityCard(
title: 'Dîner à Trastevere',
date: '11 août, 20:30',
icon: Icons.restaurant,
),
],
);
}
Widget _buildActivityCard({
required String title,
required String date,
required IconData icon,
}) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.black.withValues(alpha: 0.05),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.teal.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.teal, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
date,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
),
Icon(
Icons.chevron_right,
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
],
),
);
}
Widget _buildExpensesCard() {
final theme = Theme.of(context);
return Container(
margin: const EdgeInsets.only(top: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFDF4E3), // Light beige background
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: const Icon(
Icons.warning_amber_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dépenses',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037), // Brown text
),
),
const SizedBox(height: 4),
Text(
'Vous devez 25€ à Clara',
style: theme.textTheme.bodyMedium?.copyWith(
color: const Color(0xFF8D6E63), // Lighter brown
),
),
],
),
),
TextButton(
onPressed: () => _showComingSoon('Régler les dépenses'),
child: Text(
'Régler',
style: TextStyle(
color: const Color(0xFF5D4037),
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}