diff --git a/functions/index.js b/functions/index.js index 018a469..6134d2a 100644 --- a/functions/index.js +++ b/functions/index.js @@ -29,22 +29,24 @@ async function sendNotificationToUsers(userIds, title, body, excludeUserId, data } } - console.log(`Total tokens to send: ${tokens.length}`); + // De-duplicate tokens + const uniqueTokens = [...new Set(tokens)]; + console.log(`Total unique tokens to send: ${uniqueTokens.length} (from ${tokens.length} found)`); - if (tokens.length > 0) { + if (uniqueTokens.length > 0) { const message = { notification: { title: title, body: body, }, - tokens: tokens, + tokens: uniqueTokens, data: { click_action: "FLUTTER_NOTIFICATION_CLICK", ...data }, }; - const response = await admin.messaging().sendMulticast(message); + const response = await admin.messaging().sendEachForMulticast(message); console.log(`${response.successCount} messages were sent successfully`); if (response.failureCount > 0) { console.log('Failed notifications:', response.responses.filter(r => !r.success)); diff --git a/lib/components/activities/add_activity_bottom_sheet.dart b/lib/components/activities/add_activity_bottom_sheet.dart index a55cd1a..1cbfb36 100644 --- a/lib/components/activities/add_activity_bottom_sheet.dart +++ b/lib/components/activities/add_activity_bottom_sheet.dart @@ -26,6 +26,7 @@ class _AddActivityBottomSheetState extends State { final ErrorService _errorService = ErrorService(); ActivityCategory _selectedCategory = ActivityCategory.attraction; + DateTime? _selectedDate; bool _isLoading = false; @override @@ -150,6 +151,13 @@ class _AddActivityBottomSheetState extends State { icon: Icons.location_on, ), + const SizedBox(height: 20), + + // Date et heure (optionnel) + _buildSectionTitle('Date et heure (optionnel)'), + const SizedBox(height: 8), + _buildDateTimePicker(), + const SizedBox(height: 40), // Boutons d'action @@ -369,6 +377,7 @@ class _AddActivityBottomSheetState extends State { votes: {}, createdAt: DateTime.now(), updatedAt: DateTime.now(), + date: _selectedDate, createdBy: FirebaseAuth.instance.currentUser?.uid, ); @@ -414,4 +423,92 @@ class _AddActivityBottomSheetState extends State { return Icons.spa; } } + + Widget _buildDateTimePicker() { + final theme = Theme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; + + return InkWell( + onTap: _pickDateTime, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.2), + ), + ), + child: Row( + children: [ + Icon( + Icons.calendar_today, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + const SizedBox(width: 12), + Text( + _selectedDate != null + ? '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year} à ${_selectedDate!.hour}:${_selectedDate!.minute.toString().padLeft(2, '0')}' + : 'Choisir une date et une heure', + style: theme.textTheme.bodyMedium?.copyWith( + color: _selectedDate != null + ? theme.colorScheme.onSurface + : theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + const Spacer(), + if (_selectedDate != null) + IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + setState(() { + _selectedDate = null; + }); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + ); + } + + Future _pickDateTime() async { + final now = DateTime.now(); + final initialDate = widget.trip.startDate.isAfter(now) + ? widget.trip.startDate + : now; + + final date = await showDatePicker( + context: context, + initialDate: _selectedDate ?? initialDate, + firstDate: now.subtract(const Duration(days: 365)), + lastDate: now.add(const Duration(days: 365 * 2)), + ); + + if (date == null) return; + + if (!mounted) return; + + final time = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(_selectedDate ?? now), + ); + + if (time == null) return; + + setState(() { + _selectedDate = DateTime( + date.year, + date.month, + date.day, + time.hour, + time.minute, + ); + }); + } } diff --git a/lib/components/home/calendar/calendar_page.dart b/lib/components/home/calendar/calendar_page.dart index a00e1f6..5c23c3e 100644 --- a/lib/components/home/calendar/calendar_page.dart +++ b/lib/components/home/calendar/calendar_page.dart @@ -20,7 +20,7 @@ class CalendarPage extends StatefulWidget { class _CalendarPageState extends State { late DateTime _focusedDay; DateTime? _selectedDay; - CalendarFormat _calendarFormat = CalendarFormat.month; + CalendarFormat _calendarFormat = CalendarFormat.week; @override void initState() { @@ -70,13 +70,32 @@ class _CalendarPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final isDarkMode = theme.brightness == Brightness.dark; return Scaffold( + backgroundColor: isDarkMode + ? theme.scaffoldBackgroundColor + : Colors.white, appBar: AppBar( - title: const Text('Calendrier du voyage'), - backgroundColor: theme.colorScheme.surface, - foregroundColor: theme.colorScheme.onSurface, + title: Text( + widget.trip.title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + backgroundColor: Colors.transparent, elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, color: theme.colorScheme.onSurface), + onPressed: () => Navigator.pop(context), + ), + actions: [ + IconButton( + icon: Icon(Icons.people, color: theme.colorScheme.onSurface), + onPressed: () {}, // TODO: Show participants + ), + ], ), body: BlocBuilder( builder: (context, state) { @@ -88,8 +107,7 @@ class _CalendarPageState extends State { 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 + // Fallback if we are in search state } // Filter approved activities @@ -113,215 +131,124 @@ class _CalendarPageState extends State { scheduledActivities, ); + // Sort by time + selectedActivities.sort((a, b) => a.date!.compareTo(b.date!)); + 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; + // Calendar Strip + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isDarkMode ? theme.cardColor : Colors.grey[100], + borderRadius: BorderRadius.circular(12), + ), + child: TableCalendar( + firstDay: DateTime.now().subtract(const Duration(days: 365)), + lastDay: DateTime.now().add(const Duration(days: 365)), + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + headerStyle: HeaderStyle( + formatButtonVisible: false, + titleCentered: true, + titleTextStyle: theme.textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + leftChevronIcon: const Icon(Icons.chevron_left), + rightChevronIcon: const Icon(Icons.chevron_right), + ), + calendarStyle: CalendarStyle( + todayDecoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + selectedDecoration: const BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + ), + markerDecoration: const BoxDecoration( + color: Colors.purple, + shape: BoxShape.circle, + ), + ), + selectedDayPredicate: (day) { + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + }, + onPageChanged: (focusedDay) { _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, - ), - ), - ); + }, + eventLoader: (day) { + return _getActivitiesForDay(day, scheduledActivities); }, ), - 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(), + + const SizedBox(height: 16), + + // Timeline View 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: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Timeline + if (selectedActivities.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Center( 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} - ${DateFormat('HH:mm').format(activity.date!)}', - ), - trailing: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - context.read().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( - 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) { - _selectTimeAndSchedule( - activity, - _selectedDay!, - ); - } - }, - ), - ), - ); - }, - ), - ), - // Zone de drop pour le calendrier - DragTarget( - onWillAcceptWithDetails: (details) => true, - onAcceptWithDetails: (details) { - if (_selectedDay != null) { - _selectTimeAndSchedule( - details.data, - _selectedDay!, - ); - } - }, - builder: (context, candidateData, rejectedData) { - return Container( - height: 50, - color: candidateData.isNotEmpty - ? theme.colorScheme.primary.withValues( - alpha: 0.1, - ) - : null, - child: Center( - child: Text( - 'Glisser ici pour planifier', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), + 'Aucune activité prévue ce jour', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface.withValues( + alpha: 0.5, ), - ); - }, + ), + ), ), - ], + ) + else + ...selectedActivities.map((activity) { + return _buildTimelineItem(activity, theme); + }), + + const SizedBox(height: 32), + + // Unscheduled Activities Section + Text( + 'Activités à ajouter', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), - ], + const SizedBox(height: 16), + + if (unscheduledActivities.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + 'Toutes les activités sont planifiées !', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues( + alpha: 0.5, + ), + ), + ), + ) + else + ...unscheduledActivities.map((activity) { + return _buildUnscheduledActivityCard(activity, theme); + }), + + const SizedBox(height: 32), + ], + ), ), ), ], @@ -330,4 +257,160 @@ class _CalendarPageState extends State { ), ); } + + Widget _buildTimelineItem(Activity activity, ThemeData theme) { + final timeFormat = DateFormat('HH:mm'); // 10:00 + final endTimeFormat = DateFormat('HH:mm'); // 11:30 (simulated duration) + + // Simulate duration (1h30) + final endTime = activity.date!.add(const Duration(hours: 1, minutes: 30)); + + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Time Column + SizedBox( + width: 50, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${activity.date!.hour}h', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), + ), + + // Timeline Line + // Expanded(child: Container()), // Placeholder for line if needed + + // Activity Card + Expanded( + child: Container( + margin: const EdgeInsets.only(bottom: 24), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _getCategoryColor( + activity.category, + ).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border( + left: BorderSide( + color: _getCategoryColor(activity.category), + width: 4, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activity.name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + '${timeFormat.format(activity.date!)} - ${endTimeFormat.format(endTime)}', + style: theme.textTheme.bodyMedium?.copyWith( + color: _getCategoryColor(activity.category), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildUnscheduledActivityCard(Activity activity, ThemeData theme) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _getCategoryColor(activity.category).withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + _getCategoryIcon(activity.category), + color: _getCategoryColor(activity.category), + ), + ), + title: Text( + activity.name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + activity.category, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + trailing: IconButton( + icon: const Icon(Icons.grid_view), // Drag handle icon + onPressed: () { + if (_selectedDay != null) { + _selectTimeAndSchedule(activity, _selectedDay!); + } + }, + ), + ), + ); + } + + Color _getCategoryColor(String category) { + // Simple mapping based on category name + // You might want to use the enum if possible, but category is String in Activity model + if (category.toLowerCase().contains('musée') || + category.toLowerCase().contains('museum')) + return Colors.blue; + if (category.toLowerCase().contains('restaurant') || + category.toLowerCase().contains('food')) + return Colors.orange; + if (category.toLowerCase().contains('nature') || + category.toLowerCase().contains('park')) + return Colors.green; + if (category.toLowerCase().contains('photo') || + category.toLowerCase().contains('attraction')) + return Colors.purple; + if (category.toLowerCase().contains('détente') || + category.toLowerCase().contains('relax')) + return Colors.pink; + return Colors.teal; + } + + IconData _getCategoryIcon(String category) { + if (category.toLowerCase().contains('musée')) return Icons.museum; + if (category.toLowerCase().contains('restaurant')) return Icons.restaurant; + if (category.toLowerCase().contains('nature')) return Icons.nature; + if (category.toLowerCase().contains('photo')) return Icons.camera_alt; + if (category.toLowerCase().contains('détente')) + return Icons.icecream; // Gelato icon :) + return Icons.place; + } } diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index 8cc8665..1aaca20 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -9,7 +9,6 @@ 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'; @@ -17,6 +16,19 @@ 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'; +import 'package:intl/intl.dart'; +import 'package:travel_mate/models/activity.dart'; +import 'package:travel_mate/blocs/activity/activity_state.dart'; +import 'package:travel_mate/blocs/balance/balance_bloc.dart'; +import 'package:travel_mate/blocs/balance/balance_event.dart'; +import 'package:travel_mate/blocs/balance/balance_state.dart'; +import 'package:travel_mate/models/settlement.dart'; +import 'package:travel_mate/blocs/user/user_bloc.dart'; +import 'package:travel_mate/blocs/user/user_state.dart' as user_state; +import 'package:travel_mate/components/account/group_expenses_page.dart'; +import 'package:travel_mate/models/group.dart'; +import 'package:travel_mate/models/account.dart'; +import 'package:travel_mate/models/user_balance.dart'; class ShowTripDetailsContent extends StatefulWidget { final Trip trip; @@ -28,49 +40,48 @@ class ShowTripDetailsContent extends StatefulWidget { class _ShowTripDetailsContentState extends State { final ErrorService _errorService = ErrorService(); - final ActivityCacheService _cacheService = ActivityCacheService(); final GroupRepository _groupRepository = GroupRepository(); final UserRepository _userRepository = UserRepository(); final AccountRepository _accountRepository = AccountRepository(); + Group? _group; + Account? _account; + @override void initState() { super.initState(); - // Lancer la recherche d'activités Google en arrière-plan - _preloadGoogleActivities(); + // Charger les activités du voyage depuis la DB + if (widget.trip.id != null) { + context.read().add(LoadActivities(widget.trip.id!)); + _loadGroupAndAccount(); + } } - /// 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 - } + Future _loadGroupAndAccount() async { + if (widget.trip.id == null) return; - // Sinon, lancer la recherche avec le maximum d'activités - context.read().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, - ), - ); + try { + final group = await _groupRepository.getGroupByTripId(widget.trip.id!); + final account = await _accountRepository.getAccountByTripId( + widget.trip.id!, + ); + + if (mounted) { + setState(() { + _group = group; + _account = account; + }); + + if (group != null) { + context.read().add(LoadGroupBalances(group.id)); + } } - }); + } catch (e) { + _errorService.logError( + 'ShowTripDetailsContent', + 'Error loading group/account: $e', + ); + } } // Calculer les jours restants avant le voyage @@ -446,7 +457,19 @@ class _ShowTripDetailsContentState extends State { icon: Icons.account_balance_wallet, title: 'Dépenses', color: Colors.orange, - onTap: () => _showComingSoon('Dépenses'), + onTap: () { + if (_group != null && _account != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupExpensesPage( + group: _group!, + account: _account!, + ), + ), + ); + } + }, ), _buildActionButton( icon: Icons.map, @@ -546,13 +569,6 @@ class _ShowTripDetailsContentState extends State { ); } - void _showComingSoon(String feature) { - _errorService.showSnackbar( - message: '$feature - Fonctionnalité à venir', - isError: false, - ); - } - void _showOptionsMenu() { final theme = Theme.of(context); @@ -963,46 +979,87 @@ class _ShowTripDetailsContentState extends State { Widget _buildNextActivitiesSection() { final theme = Theme.of(context); - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + + return BlocBuilder( + builder: (context, state) { + List activities = []; + if (state is ActivityLoaded) { + activities = state.activities; + } + + // Filter scheduled activities and sort by date + final scheduledActivities = activities + .where((a) => a.date != null && a.date!.isAfter(DateTime.now())) + .toList(); + + scheduledActivities.sort((a, b) => a.date!.compareTo(b.date!)); + + // Take next 3 activities + final nextActivities = scheduledActivities.take(3).toList(); + + if (nextActivities.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( 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, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Prochaines activités', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), ), - ), + TextButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CalendarPage(trip: widget.trip), + ), + ), + child: Text( + 'Voir calendrier', + style: TextStyle( + color: Colors.teal, + fontWeight: FontWeight.w600, + ), + ), + ), + ], ), + const SizedBox(height: 8), + ...nextActivities.map((activity) { + if (activity.date == null) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildActivityCard( + title: activity.name, + date: DateFormat( + 'd MMM, HH:mm', + 'fr_FR', + ).format(activity.date!), + icon: _getCategoryIcon(activity.category), + ), + ); + }), ], - ), - 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, - ), - ], + ); + }, ); } + IconData _getCategoryIcon(String category) { + if (category.toLowerCase().contains('musée')) return Icons.museum; + if (category.toLowerCase().contains('restaurant')) return Icons.restaurant; + if (category.toLowerCase().contains('nature')) return Icons.nature; + if (category.toLowerCase().contains('photo')) return Icons.camera_alt; + if (category.toLowerCase().contains('détente')) return Icons.icecream; + return Icons.place; + } + Widget _buildActivityCard({ required String title, required String date, @@ -1074,61 +1131,193 @@ class _ShowTripDetailsContentState extends State { 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, + return BlocBuilder( + builder: (context, state) { + String balanceText = 'Chargement...'; + bool isLoading = state is BalanceLoading; + bool isPositive = true; + + if (state is GroupBalancesLoaded) { + final userState = context.read().state; + if (userState is user_state.UserLoaded) { + final currentUserId = userState.user.id; + + // Filter settlements involving the current user + final mySettlements = state.settlements + .where( + (s) => + !s.isCompleted && + (s.fromUserId == currentUserId || + s.toUserId == currentUserId), + ) + .toList(); + + if (mySettlements.isEmpty) { + // Check if user has a balance of 0 + final myBalanceObj = state.balances.firstWhere( + (b) => b.userId == currentUserId, + orElse: () => const UserBalance( + userId: '', + userName: '', + totalPaid: 0, + totalOwed: 0, + balance: 0, + ), + ); + + if (myBalanceObj.balance.abs() < 0.01) { + balanceText = 'Vous êtes à jour'; + } else { + // Fallback to total balance if no settlements found but balance exists + isPositive = myBalanceObj.balance >= 0; + final amountStr = + '${myBalanceObj.balance.abs().toStringAsFixed(2)} €'; + balanceText = isPositive + ? 'On vous doit $amountStr' + : 'Vous devez $amountStr'; + } + } else { + // Construct detailed string + final debtsToPay = mySettlements + .where((s) => s.fromUserId == currentUserId) + .toList(); + final debtsToReceive = mySettlements + .where((s) => s.toUserId == currentUserId) + .toList(); + + if (debtsToPay.isNotEmpty) { + isPositive = false; + final details = debtsToPay + .map( + (s) => + '${s.amount.toStringAsFixed(2)}€ à ${s.toUserName}', + ) + .join(' et '); + balanceText = 'Vous devez $details'; + } else if (debtsToReceive.isNotEmpty) { + isPositive = true; + final details = debtsToReceive + .map( + (s) => + '${s.amount.toStringAsFixed(2)}€ de ${s.fromUserName}', + ) + .join(' et '); + balanceText = + 'On vous doit $details'; // Or "X owes you..." but "On vous doit" is generic enough or we can be specific + // Let's be specific as requested: "X doit vous payer..." or similar? + // The user asked: "vous devez 21 euros à John..." (active voice for user paying). + // For receiving, "John vous doit 21 euros..." would be symmetric. + // Let's try to match the requested format for paying first. + + if (debtsToReceive.length == 1) { + balanceText = + '${debtsToReceive.first.fromUserName} vous doit ${debtsToReceive.first.amount.toStringAsFixed(2)}€'; + } else { + balanceText = + debtsToReceive + .map( + (s) => + '${s.fromUserName} (${s.amount.toStringAsFixed(2)}€)', + ) + .join(' et ') + + ' vous doivent de l\'argent'; + } + } + } + } + } + + return GestureDetector( + onTap: () { + if (_group != null && _account != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + GroupExpensesPage(group: _group!, account: _account!), + ), + ); + } + }, + child: 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: const Icon( - Icons.warning_amber_rounded, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Text( - 'Dépenses', - style: theme.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - color: const Color(0xFF5D4037), // Brown text + 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(height: 4), - Text( - 'Vous devez 25€ à Clara', - style: theme.textTheme.bodyMedium?.copyWith( - color: const Color(0xFF8D6E63), // Lighter brown + 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), + if (isLoading) + const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Text( + balanceText, + style: theme.textTheme.bodyMedium?.copyWith( + color: const Color(0xFF8D6E63), // Lighter brown + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + TextButton( + onPressed: () { + if (_group != null && _account != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupExpensesPage( + group: _group!, + account: _account!, + ), + ), + ); + } + }, + child: Text( + 'Régler', + style: TextStyle( + color: const Color(0xFF5D4037), + fontWeight: FontWeight.bold, + ), ), ), ], ), ), - TextButton( - onPressed: () => _showComingSoon('Régler les dépenses'), - child: Text( - 'Régler', - style: TextStyle( - color: const Color(0xFF5D4037), - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), + ); + }, ); } }