import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/models/trip_invitation.dart'; import 'package:travel_mate/repositories/trip_invitation_repository.dart'; import 'package:travel_mate/services/error_service.dart'; /// Affiche la boîte de réception des invitations de voyage. /// /// Cette page permet de consulter toutes les invitations reçues et de répondre /// aux invitations en attente (`pending`) via les actions accepter/refuser. class TripInvitationsPage extends StatelessWidget { /// Repository utilisé pour charger et mettre à jour les invitations. final TripInvitationDataSource repository; /// Identifiant utilisateur injecté pour les tests ou contextes spécifiques. final String? userIdOverride; /// Crée la page des invitations. /// /// [repository] peut être injecté pour les tests; sinon un repository réel est utilisé. TripInvitationsPage({ super.key, TripInvitationDataSource? repository, this.userIdOverride, }) : repository = repository ?? TripInvitationRepository(); /// Retourne les invitations correspondant au [status] demandé. /// /// [status] peut valoir `pending`, `accepted` ou `rejected`. List _filterByStatus( List invitations, String status, ) { return invitations .where((invitation) => invitation.status == status) .toList(growable: false); } /// Envoie la réponse utilisateur pour une invitation. /// /// [isAccepted] à `true` accepte l'invitation, sinon la refuse. /// Un feedback visuel est affiché à l'utilisateur. Future _respondToInvitation({ required BuildContext context, required String invitationId, required bool isAccepted, }) async { try { await repository.respondToInvitation( invitationId: invitationId, isAccepted: isAccepted, ); if (!context.mounted) { return; } ErrorService().showSnackbar( message: isAccepted ? 'Invitation acceptée' : 'Invitation refusée', isError: false, ); } catch (e) { if (!context.mounted) { return; } ErrorService().showError(message: 'Erreur lors de la réponse: $e'); } } @override Widget build(BuildContext context) { if (userIdOverride != null && userIdOverride!.isNotEmpty) { return _InvitationsScaffold( userId: userIdOverride!, repository: repository, onRespond: ({required String invitationId, required bool isAccepted}) { return _respondToInvitation( context: context, invitationId: invitationId, isAccepted: isAccepted, ); }, filterByStatus: _filterByStatus, ); } return BlocBuilder( builder: (context, state) { if (state is! user_state.UserLoaded) { return Scaffold( appBar: AppBar(title: const Text('Invitations')), body: const Center(child: Text('Utilisateur non chargé')), ); } return _InvitationsScaffold( userId: state.user.id, repository: repository, onRespond: ({required String invitationId, required bool isAccepted}) { return _respondToInvitation( context: context, invitationId: invitationId, isAccepted: isAccepted, ); }, filterByStatus: _filterByStatus, ); }, ); } } /// Échafaudage principal des invitations avec onglets par statut. class _InvitationsScaffold extends StatelessWidget { /// Identifiant utilisateur des invitations à afficher. final String userId; /// Source de données des invitations. final TripInvitationDataSource repository; /// Callback appelé lors d'une réponse utilisateur. final Future Function({ required String invitationId, required bool isAccepted, }) onRespond; /// Fonction de filtrage par statut. final List Function( List invitations, String status, ) filterByStatus; /// Crée l'échafaudage d'affichage des invitations. const _InvitationsScaffold({ required this.userId, required this.repository, required this.onRespond, required this.filterByStatus, }); @override Widget build(BuildContext context) { return DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( title: const Text('Invitations'), bottom: const TabBar( tabs: [ Tab(text: 'En attente'), Tab(text: 'Acceptées'), Tab(text: 'Refusées'), ], ), ), body: StreamBuilder>( stream: repository.watchInvitationsForUser(userId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center( child: Text('Erreur de chargement: ${snapshot.error}'), ); } final invitations = snapshot.data ?? const []; final pending = filterByStatus(invitations, 'pending'); final accepted = filterByStatus(invitations, 'accepted'); final rejected = filterByStatus(invitations, 'rejected'); return TabBarView( children: [ _InvitationsList( invitations: pending, onAccept: (id) => onRespond(invitationId: id, isAccepted: true), onReject: (id) => onRespond(invitationId: id, isAccepted: false), ), _InvitationsList(invitations: accepted), _InvitationsList(invitations: rejected), ], ); }, ), ), ); } } /// Affiche une liste d'invitations avec actions éventuelles. class _InvitationsList extends StatelessWidget { /// Invitations à afficher. final List invitations; /// Callback appelé lors d'une acceptation. final Future Function(String invitationId)? onAccept; /// Callback appelé lors d'un refus. final Future Function(String invitationId)? onReject; /// Crée une liste d'invitations. const _InvitationsList({ required this.invitations, this.onAccept, this.onReject, }); @override Widget build(BuildContext context) { if (invitations.isEmpty) { return const Center(child: Text('Aucune invitation')); } return ListView.separated( padding: const EdgeInsets.all(12), itemCount: invitations.length, separatorBuilder: (context, _) => const SizedBox(height: 8), itemBuilder: (context, index) { final invitation = invitations[index]; final isPending = invitation.status == 'pending'; return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( invitation.tripTitle, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text('Invité par: ${invitation.inviterName}'), Text( 'Reçu: ${invitation.createdAt.day}/${invitation.createdAt.month}/${invitation.createdAt.year}', ), if (isPending) ...[ const SizedBox(height: 12), Row( children: [ OutlinedButton( onPressed: onReject == null ? null : () => onReject!(invitation.id), child: const Text('Refuser'), ), const SizedBox(width: 8), ElevatedButton( onPressed: onAccept == null ? null : () => onAccept!(invitation.id), child: const Text('Accepter'), ), ], ), ], ], ), ), ); }, ); } }